Skip to content

model

definition model

By defining ormar models, you can get corresponding Pydantic models and Sqlalchemy tables for free. They are managed in the background, you don't have to create them yourself.

model class

To build an ormar model, you simply inherit the ormar.Model class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

definition field

Next assign one or more fields as class-level variables.

Basic field types

Each table must have a primary key column, which you specify by setting Primary_key=True on the selected field.

Only one primary key column is allowed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

!!!Warning Not assigning the Primary_key column or assigning multiple columns to each model will throw a ModelDefinitionError exception.

By default, if you assign a primary key to an integer field, the autoincrement option is set to true.

You can disable it by passing autoincrement=False.

1
id: int = ormar.Integer(primary_key=True, autoincrement=False)

non-database field

Note that if you need a plain pydantic field in your model (to store values or pass some values on the model), you can define a field in pydantic as usual.

Fields created like this will be added to pydantic model fields-> so validation is done according to field type and also appears in model_dump() and model_dump_json() results.

The difference is that these fields are not saved in the database. So they are not included in the underlying sqlalchemy columns or table variables (check the internals below to see how to access them when needed).

The pydantic field will then not be included in the migration or any database operations (such as save, update, etc.).

Fields like this can be passed into the payload in fastapi requests and will be returned in fastapi responses (only if, of course, you set their value somewhere in your code, because that value is not retrieved from the database. If you pass a value in fastapirequest and return the same instance fastapi constructed for you in request_model, you should return exactly the same value in the response.).

!!! The warning pyadic field must always be optional and cannot be changed (otherwise database load validation will fail)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import databases
import ormar
import pydantic
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)
    non_db_field: str = pydantic.Field(max_length=100)

If you set the pydantic field with default parameters and do not pass the actual value in the request, you will always get the default value. Since it can be a function, you can set default= datetime.datetime.now and get the current timestamp every time you call the endpoint, etc.

Non-database fields in Fastapi

Note that both pydantic and calculated_fields modifier fields can be included/excluded in the model_dump() and fastapi responses, with corresponding include/exclude and response_model_include/response_model_exclude.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# <==part of related code removed for clarity==>
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
    engine=sqlalchemy.create_engine(DATABASE_URL),
)


class User(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="users2")

    id: int = ormar.Integer(primary_key=True)
    email: str = ormar.String(max_length=255, nullable=False)
    password: str = ormar.String(max_length=255)
    first_name: str = ormar.String(max_length=255)
    last_name: str = ormar.String(max_length=255)
    category: str = ormar.String(max_length=255, nullable=True)
    timestamp: datetime.datetime = pydantic.Field(
        default=datetime.datetime.now
    )

# <==part of related code removed for clarity==>
app = FastAPI()

@app.post("/users/")
async def create_user(user: User):
    return await user.save()

# <==part of related code removed for clarity==>

def test_excluding_fields_in_endpoints():
    client = TestClient(app)
    with client as client:
        timestamp = datetime.datetime.now()

        user = {
            "email": "test@domain.com",
            "password": "^*^%A*DA*IAAA",
            "first_name": "John",
            "last_name": "Doe",
            "timestamp": str(timestamp),
        }
        response = client.post("/users/", json=user)
        assert list(response.json().keys()) == [
            "id",
            "email",
            "first_name",
            "last_name",
            "category",
            "timestamp",
        ]
        # returned is the same timestamp
        assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")


# <==part of related code removed for clarity==>

Field name and column name

By default, field names are used for the underlying pydantic model and sqlalchemy tables.

If for some reason you want to change the name in the database but keep it in the model, you can do so by specifying the name parameter during field declaration

Here's an example model with a name change

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import databases
import ormar
import sqlalchemy

DATABASE_URl = "sqlite:///test.db"

database = databases.Database(DATABASE_URl, force_rollback=True)
metadata = sqlalchemy.MetaData()


class Child(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="children",
    )

    id: int = ormar.Integer(name="child_id", primary_key=True)
    first_name: str = ormar.String(name="fname", max_length=100)
    last_name: str = ormar.String(name="lname", max_length=100)
    born_year: int = ormar.Integer(name="year_born", nullable=True)

Note that you can also change the foreign key column name

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from typing import Optional

import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///test.db", force_rollback=True)
metadata = sqlalchemy.MetaData()


class Artist(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="artists",
    )

    id: int = ormar.Integer(name="artist_id", primary_key=True)
    first_name: str = ormar.String(name="fname", max_length=100)
    last_name: str = ormar.String(name="lname", max_length=100)
    born_year: int = ormar.Integer(name="year")


class Album(ormar.Model):

    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="music_albums",
    )

    id: int = ormar.Integer(name="album_id", primary_key=True)
    name: str = ormar.String(name="album_name", max_length=100)
    artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id")

However, at this time you cannot change the names of the ManyToMany columns because they pass through other models anyway.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import databases
import ormar
import sqlalchemy

DATABASE_URl = "sqlite:///test.db"

database = databases.Database(DATABASE_URl, force_rollback=True)
metadata = sqlalchemy.MetaData()


class Child(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="children",
    )

    id: int = ormar.Integer(name="child_id", primary_key=True)
    first_name: str = ormar.String(name="fname", max_length=100)
    last_name: str = ormar.String(name="lname", max_length=100)
    born_year: int = ormar.Integer(name="year_born", nullable=True)


class ArtistChildren(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="children_x_artists",
    )


class Artist(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        tablename="artists",
    )

    id: int = ormar.Integer(name="artist_id", primary_key=True)
    first_name: str = ormar.String(name="fname", max_length=100)
    last_name: str = ormar.String(name="lname", max_length=100)
    born_year: int = ormar.Integer(name="year")
    children = ormar.ManyToMany(Child, through=ArtistChildren)

Overwrite default query set

If you want to customize the queries ormar runs, you can define your own query set class (extended ormar QuerySet) in the model class, the default is QuerySet

You can provide a new class as a queryset_class parameter in the ormar_config of the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import ormar
from ormar.queryset.queryset import QuerySet
from fastapi import HTTPException


class MyQuerySetClass(QuerySet):

    async def first_or_404(self, *args, **kwargs):
        entity = await self.get_or_none(*args, **kwargs) 
        if entity is None:
            # in fastapi or starlette
            raise HTTPException(404)


class Book(ormar.Model):
    ormar_config = base_ormar_config.copy(
        queryset_class=MyQuerySetClass,
        tablename="book",
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=32)


# when book not found, raise `404` in your view.
book = await Book.objects.first_or_404(name="123")

type prompt

Note that for better IDE support and mypy checking, you can provide type hints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

Note that type hinting is optional, so a fully valid ormar code would look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id = ormar.Integer(primary_key=True)
    name = ormar.String(max_length=100)
    completed = ormar.Boolean(default=False)

!!!Warning Even if you use type hints, ormar won't use them to construct pydantic fields!

1
2
Type hints are there only to support static checkers and linting, 
`ormar` construct annotations used by `pydantic` from own fields.

dependence

Because ormar relies on databases and sqlchemy-core for database joins and table creation, you need to assign two special parameters to each model.

database

One is a database instance created using a database URL in sqlalchemy connection string format.

The instance created needs to be passed to each model using the ormar_config object database parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

!!!Tip You only need to create a database instance once and use it for all models. If you want to use multiple databases, you can create multiple databases.

alchemy

The second dependency is the sqlalchemy MetaData instance.

The created instance needs to be passed to each model using the ormar_config object metadata parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

!!!Tip You only need to create MetaData instance once and use it for all models. If you want to use multiple databases, you can create multiple databases.

best practices

Note that ormar requires a field named ormar_config, which is an instance of the OrmarConfig class. To simplify configuration management, the OrmarConfig class provides copy methods. Therefore, you should create a base object and use copies of it in all models, rather than providing the same parameters for all models over and over again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from typing import Optional

import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
)


class Artist(ormar.Model):
    # note that tablename is optional
    # if not provided ormar will user class.__name__.lower()+'s'
    # -> artists in this example
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    first_name: str = ormar.String(max_length=100)
    last_name: str = ormar.String(max_length=100)
    born_year: int = ormar.Integer(name="year")


class Album(ormar.Model):
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    artist: Optional[Artist] = ormar.ForeignKey(Artist)

table name

By default, table names are created based on model class names, that is, lowercase names plus "s."

You can override this parameter by supplying the table name parameter of the ormar_config object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):

    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        # if you omit this parameter it will be created automatically
        # as class.__name__.lower()+'s' -> "courses" in this example
        tablename="my_courses",
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

constraint condition

At the model level, you can also set model-side constraints on sql columns.

Currently only IndexColumns, UniqueColumns, and CheckColumns constraints are supported.

!!!note Note that you should use these two constraints only if you want to set a name on the constraint or index on multiple columns, otherwise the index and unique attributes on the ormar field are preferred.

!!!tip To learn more about column constraints such as primary key, unique, foreign key, and so on, visit Fields.

Unique columns

You can set this parameter by supplying the ormar_config object constraint parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        # define your constraints in OrmarConfig of the model
        # it's a list that can contain multiple constraints
        # hera a combination of name and column will have to be unique in db
        constraints=[ormar.UniqueColumns("name", "completed")],
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

!!!note Note that constraints are for combinations of columns that should be unique. To make a column unique, use a unique generic parameter. Of course, you can use this parameter to make many columns unique, but each column will be checked separately.

index column

You can set this parameter by supplying the ormar_config object constraint parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        # define your constraints in OrmarConfig of the model
        # it's a list that can contain multiple constraints
        # hera a combination of name and column will have a compound index in the db
        constraints=[ormar.IndexColumns("name", "completed")],
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

!!!note Note that constraints are combinations of columns that should be included in an index. To set a column index, use a unique common parameter. Of course, you can set many columns as indexes using this parameter, but each column will be a separate index.

check column

You can set this parameter by supplying the ormar_config object constraint parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import datetime

import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
        # define your constraints in OrmarConfig of the model
        # it's a list that can contain multiple constraints
        # hera a combination of name and column will have a level check in the db
        constraints=[
            ormar.CheckColumns("start_time < end_time", name="date_check"),
        ],
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    start_date: datetime.date = ormar.Date()
    end_date: datetime.date = ormar.Date()

!!!Note that some databases do not actively support checking constraints (for example MySQL).

cantilever construction

Since each ormar.Model is also a pydantic model, you may need to adjust the pydantic configuration settings.

The way to do this in pydantic is to adjust the settings on the model_config dictionary provided to the model, which works exactly the same for the ormar model.

Therefore, in order to set your own preferences, you need to provide not only the ormar_config class to the model, but also the model_config = ConfigDict() class.

!!!Note To learn more about available settings, visit the pydantic configuration page.

Note that ormar will do it for you if you do not provide your own configuration. The default configurations provided are as follows:

1
model_config = ConfigDict(validate_assignment=True, ser_json_bytes="base64")

So to override the settings or provide your own sample model, look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import databases
import ormar
import pydantic
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()


class Course(ormar.Model):
    ormar_config = ormar.OrmarConfig(
        database=database,
        metadata=metadata,
    )

    model_config = pydantic.ConfigDict(frozen=True)

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)

Additional fields in the model

By default, ormar prohibits you from passing extra fields to the model.

If you try to do so, a ModelError will be raised.

Because extra fields cannot be saved in the database, disabling such fields by default seems like a viable option.

In contrast, in pydantic, the default option is to ignore such extra fields, so ormar provides the ormar_config.extra setting to work the same way.

To ignore extra fields passed to ormar, set this setting to Extra.ignore instead of the default Extra. bid.

Note that ormar is not allowed to accept extra fields, you can only ignore them or disable them (throwing an exception if they exist)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from ormar import Extra, OrmarConfig

class Child(ormar.Model):
    ormar_config = OrmarConfig(
        tablename="children",
        extra=Extra.ignore  # set extra setting to prevent exceptions on extra fields presence
    )

    id: int = ormar.Integer(name="child_id", primary_key=True)
    first_name: str = ormar.String(name="fname", max_length=100)
    last_name: str = ormar.String(name="lname", max_length=100)

To set the same settings on all models, check best practices and the base_ormar_config concept.

Sort by model

By default, when querying a database with a given model, the models are sorted in ascending order by the Primary_key column. If you want to change the default behavior, you can do so by supplying the orders_by parameter to the model or mar_config object.

Example default sorting (unspecified-hence specified by primary key):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
)


# default sort by column id ascending
class Author(ormar.Model):
    ormar_config = base_ormar_config.copy(
        tablename="authors",
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)

modified

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL),
    metadata=sqlalchemy.MetaData(),
)

# now default sort by name descending
class Author(ormar.Model):
    ormar_config = base_ormar_config.copy(
        orders_by = ["-name"],
        tablename="authors",
    )

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)

model initialization

There are two ways to create and save model instances in the database.

If you plan to modify the instance later when you execute the program, you can start the model as a normal class and wait for the save() call.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import asyncio

import databases
import ormar
import sqlalchemy
from examples import create_drop_database

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Course(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="courses")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)


@create_drop_database(base_config=ormar_base_config)
async def run_query():
    course = Course(name="Painting for dummies", completed=False)
    await course.save()

    await Course.objects.create(name="Painting for dummies", completed=False)


asyncio.run(run_query())

If you want to launch the model and save it in the database at the same time, use QuerySet's method create().

To create multiple objects at once, you can use the bulk_create() QuerySet method.

Each model has a query set initialized as object parameters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import asyncio

import databases
import ormar
import sqlalchemy
from examples import create_drop_database

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Course(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="courses")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    completed: bool = ormar.Boolean(default=False)


@create_drop_database(base_config=ormar_base_config)
async def run_query():
    course = Course(name="Painting for dummies", completed=False)
    await course.save()

    await Course.objects.create(name="Painting for dummies", completed=False)


asyncio.run(run_query())

!!!info To learn more about query sets (including batch operations) and available methods, visit Query

model preservation status

Each model instance is a separate python object that knows nothing about each other.

1
2
3
4
5
6
7
8
track1 = await Track.objects.get(name='The Bird')
track2 = await Track.objects.get(name='The Bird')
assert track1 == track2 # True

track1.name = 'The Bird2'
await track1.save()
assert track1.name == track2.name # False
# track2 does not update and knows nothing about track1

The object itself has a save state, set as follows:

  • Save model after saving/updating/loading/updating insert method on model create/get/first/all/get_or_create/update_or_create method after* model is saved
  • The model is saved when passed to bulk_update and bulk_create
  • Save the model after adding/deleting ManyToMany related objects (automatically save/delete through model instances)
  • The model is not saved after changing any of its own fields (including pk as an alias for Model.pk)
  • Model not saved after adding/deleting ForeignKey related objects (fk column not saved)
  • does not save the model after instantiation with__init__(without QuerySet.create or before calling save)

You can use the ModelInstance.saved property to check if the model has been saved