23.3 What changed in models and fields
The changes from Django 5.0 to this commit in how a model and its fields work: db_default= and GeneratedField, the on_delete actions left to the database, CompositePrimaryKey, BigAutoField as the automatic key, choices, Model.NotUpdated, the constraints, and smaller changes to saving and to the field classes.
Over these releases a model came to leave more to its database, a default, a computed column, a delete, and a save learnt to read back what it made. And 6.0 removed what 5.0 and 5.1 had deprecated. Old habits fail, or are answered otherwise:
# model_habits.py at this commit: petrel is a saved Ship, leith and bergen are Ports, a Berth's key is port and number
petrel.save(False)
TypeError: Model.save() takes 1 positional argument but 2 were given
models.CheckConstraint(check=Q(tonnage__gt=0), name="positive")
TypeError: CheckConstraint.__init__() got an unexpected keyword argument 'check'
Ship(pk=999, name="Ghost", tonnage=1, home=bergen).save(force_update=True)
Ship.NotUpdated: Forced update did not affect any rows.
Berth(port=leith, metres=60) == Berth(port=leith, metres=60)
False
Values the database supplies: db_default and GeneratedField
A field may have a default that the database applies, db_default= (5.0), beside the default= that Django applies to a new instance. A new instance given no value holds a DatabaseDefault in the value’s place, unless the field also has default=, and the insert leaves the column out where it can, or writes "DEFAULT" or the default’s expression.
GeneratedField (5.0) is a column the database computes from others in its row, and no save writes it. Since 6.0 a save reads back such a field, and one assigned an expression such as F("tonnage") + 20, which the release notes say it did not do before. Model asks for both in the statement’s "RETURNING" clause on SQLite, PostgreSQL and Oracle, and on MariaDB for an insert. On MySQL, and on MariaDB for an update, Model removes the attribute, so that the next read fetches it.
Since 6.1 Django makes virtual generated columns on PostgreSQL 18 and stored ones on Oracle 23.7, each having had only the other kind: GeneratedField chooses, and a check refuses the kind a database lacks. Since 5.2 a constraint’s validation handles a generated field, taking it as its expression over the instance’s values. The field classes tells the field, and Validating an instance the validation.
A delete left to the database: DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT
Before 6.1 every on_delete was a Python function, which the Collector calls for each relation it follows, so that a cascade or a null set was Django’s own work. Since 6.1 DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT leave the work to the foreign key’s own ON DELETE clause. Each is a DatabaseOnDelete, which stands with DO_NOTHING in SKIP_COLLECTION, so the collector that a delete makes does not follow the relation, fetches none of the rows the database removes, and sends them no signal. The release notes give the fetch saved as the reason (docs/releases/6.1.txt). The checks refuse DB_SET_NULL on a field that cannot be null, DB_SET_DEFAULT on one without db_default=, and a chain of references that mixes the database’s actions with Python’s. Deleting: the Collector tells it.
A key of several columns: CompositePrimaryKey
CompositePrimaryKey (5.2) is a field with no column, assigned to "pk", that names the fields making up the key; its value is a tuple of theirs. In 6.0 QuerySet.raw came to accept such a model (What changed in querysets), and a subquery returning such a key to serve lookups other than "in" (What changed in expressions, lookups and the compiler). Also in 6.0, Model began to refuse, as "models.E049", a ForeignObject with several from_fields= named in "indexes", "constraints" or "unique_together".
Model._is_pk_set says whether a key is set: not when it is None, not when a part of a tuple is, and since 6.1 not when it is the DatabaseDefault of a key the database will supply. Model.__eq__ asks it, so an unsaved instance with such a key is equal only to itself, as the two Berths of the run are; the release notes say those with a composite or a db_default= key compared equal to others before 6.2 (docs/releases/6.2.txt). An instance and its state tells it.
The automatic key: BigAutoField
DEFAULT_AUTO_FIELD has been "django by default since 6.0, where it was "django. Options picks the class of the "id" a model with no primary key is given: that one, unless the application’s AppConfig names another. A project that set neither now gets 64-bit keys, and the autodetector, comparing a key made under the old default with the model, makes an AlterField for it. The check models.W042, which had warned of such keys since 3.2, is gone, and the project and application templates no longer write either (What changed in settings, startup and the commands).
Choices: a mapping, a callable or an enumeration
Since 5.0 choices= may be a mapping, a callable or an enumeration class itself, as in the harbour’s choices=Rank, where it had to be a list of pairs or the enumeration’s ChoicesType. The setter of Field.choices passes the value to normalize_choices, which makes the list of pairs and wraps a callable in a CallableChoiceIterator, called each time the choices are read. The metaclass once named ChoicesMeta was renamed ChoicesType in 5.0, and the old name went in 6.0. In 6.1 the blank choice that Field puts first took BLANK_CHOICE_LABEL as its label in place of dashes, which USE_BLANK_CHOICE_DASH brings back until Django 2028 (What changed in forms and validators). Choices: normalize_choices and the choice iterators tells it.
Model.NotUpdated
Since 6.0 a save told to update, by force_update=True or update_fields=, that matches no row raises the model’s Model.NotUpdated in place of a plain DatabaseError (Model; Saving an instance). ModelBase makes it a subclass of the same class on each base that is a model and not abstract, or else of ObjectNotUpdated and DatabaseError. Its comment gives the second base as backward compatibility, an except clause not consulting __subclasshook__, so code that catches DatabaseError still catches it (ModelBase; ModelBase: a class statement becomes a model).
Constraints
CheckConstraint has taken its condition as condition= since 5.1, which deprecated check=, removed in 6.0. The name went to a method. Since 6.0 BaseConstraint returns the system check’s messages for a model and a connection, and Model calls it for each database that may hold the model.
BaseConstraint (5.0) sets the code of the ValidationError that a failed validation raises. Giving the name or the message by position, deprecated in 5.0, is refused since 6.0. UniqueConstraint (5.0) asks the database to treat nulls as distinct or not, where its features allow, as PostgreSQL’s do; with False the validation counts a null as a value that can be taken.
Until 5.2 a UniqueConstraint over fields and without a condition answered with a unique field’s message whatever it had been given. Now a message given is used, with the code given. The release notes say the code is always used; at this commit a code without a message is not, the test being of the message alone. Given only violation_error_code="taken", a constraint over "name" raised for a second Petrel an error coded "unique".
The Meta option index_together was removed in 5.1. Options refuses it with a TypeError, and an Index in "indexes" takes its place. AlterIndexTogether stays, so that old migrations still apply. Constraints and indexes tells the rest.
Smaller changes
The deprecations among these are in What still warns: the deprecations and the releases that end them.
| name | what changed | release | now |
|---|---|---|---|
Model.save, Model.asave |
what changed positional arguments deprecated, then refused | release 5.1, 6.0 | now Model.save django/ |
force_insert= |
what changed may be a tuple of parent classes whose tables are forced too | release 5.0 | now Model django/ |
Model |
what changed takes from_queryset=, the queryset to reload from |
release 5.1 | now Model django/ |
Field.pre_save |
what changed may be called more than once in one save | release 6.0 | now Field.pre_save django/Model django/ |
DecimalField |
what changed both sizes may be left out where the backend allows: SQLite, PostgreSQL, Oracle | release 6.1 | now Decimal django/ |
CharField |
what changed may be made without max_length= on SQLite, as on PostgreSQL |
release 5.2 | now Char django/ |
Generic |
what changed a bad IPv6 address raises ValidationError in a save or a filter too, and white space is stripped |
release 6.2 | now Generic django/ |
BinaryField |
what changed a string that is not strict Base64 raises ValidationError |
release 6.1 | now Binary django/ |
FileField |
what changed saving a file with no name raises FieldError |
release 5.1 | now File django/ |
ImageField |
what changed measured on post_init only for a field with a width or height field (5.0), and after a save from the content, not the stored file (5.1) |
release 5.0, 5.1 | now File fields |
m2m_changed |
what changed receivers get "raw", true when serialized data sets the relation |
release 6.1 | now Many-to-many relations |
| a field’s get_placeholder | what changed deprecated for get_, which returns the SQL and its parameters |
release 6.1 | now Field django/ |
get_cache_name of FieldCacheMixin |
what changed replaced by Field: deprecated, then removed |
release 5.1, 6.0 | now FieldCacheMixin django/ |
UUIDField on MariaDB |
what changed a "uuid" column, not "char(32)" |
release 5.0 | now What changed in the database backends and migrations |