8.12 Deleting: the Collector
Model.delete sends no "DELETE" of its own. It hands the instance to a Collector, which first gathers everything the delete reaches, by following each foreign key and one-to-one field that points at the instance’s model and doing what that relation’s on_delete says, and then deletes what it gathered: first the rows it never fetched, then the instances it holds, one model at a time.
A row is seldom alone. Other rows hold its key in a foreign key’s column, and each of those relations was declared with an on_delete: what is to become of a row when the row it points at is deleted. It is deleted too, or the delete is refused, or its column is set to null. Django does most of these itself, in Python, and to do them it has to find the rows.
So a delete is two jobs, and one object does both. Collector deletes nothing. It goes from the instance to the rows that point at it, and from those to the rows that point at them, and notes for each set of rows what is to be done with it. Collector.delete then does what was noted, and begins a transaction for it where none is open, except on the short way it has for a single instance. A delete that PROTECT or RESTRICT refuses is refused by the first of the two, before any statement of the second is sent.
The examples are the models of the harbour (Models and fields). All but three of their foreign keys are declared with CASCADE, and so are the ones Django made itself: the two keys of Voyage_calls, the table of the many-to-many field "calls", and "sailor_ptr", the OneToOneField that the metaclass made as the link from Officer to Sailor. The three are "home" of Ship, which is PROTECT, its nullable one-to-one field "captain", which is SET_NULL, and "book", the foreign key from a LogEntry of the office to its Logbook, which is DB_CASCADE.
This is a voyage being deleted. Rows of two tables point at a voyage: its ports of call in Voyage_calls, and the office’s manifests, a Manifest being what a voyage carried, with a foreign key to it. Nothing is fetched, and each table loses its rows in one statement:
Collector.collect([<Voyage: Voyage object (2)>])
Collector.can_fast_delete([<Voyage: Voyage object (2)>]) -> False
Collector.add([<Voyage: Voyage object (2)>]) -> not already collected: [<Voyage: Voyage object (2)>]
Collector.can_fast_delete(the class Voyage_calls, from_field=Voyage_calls.voyage) -> True
Collector.can_fast_delete(the class Manifest, from_field=Manifest.voyage) -> True
Collector.delete
Collector.sort
Collector.can_fast_delete(<Voyage: Voyage object (2)>) -> False
Atomic.__enter__ (savepoint=False)
SQL BEGIN
QuerySet._raw_delete (a QuerySet of Voyage_calls, not yet fetched)
SQL DELETE FROM "fleet_voyage_calls" WHERE "fleet_voyage_calls"."voyage_id" IN (%s) with params (2,)
QuerySet._raw_delete (a QuerySet of Manifest, not yet fetched)
SQL DELETE FROM "office_manifest" WHERE "office_manifest"."voyage_id" IN (%s) with params (2,)
DeleteQuery.delete_batch([2]) (Voyage)
SQL DELETE FROM "fleet_voyage" WHERE "fleet_voyage"."id" IN (%s) with params (2,)
Atomic.__exit__
BaseDatabaseWrapper.commit
-> (2, {'fleet.Voyage_calls': 1, 'fleet.Voyage': 1})
Under Collector the collector entered the voyage in what it holds, and kept a queryset, not fetched, for each of the two models that point at it; the two lines that made the querysets are left out. Under Collector.delete is the transaction: a "DELETE" for each queryset, on the foreign key since no row’s own key was ever read, and one for the voyage by its key. This voyage had no manifest, and the counts do not mention the statement that removed nothing.
What Model.delete does
Model.delete refuses an instance whose primary key is not set, with a ValueError whose message names the model and the attribute that holds no key. Otherwise it settles the database, which is the alias it was given or else what the router’s ConnectionRouter returns for the instance, makes a Collector for that alias, calls Collector with the instance in a list, and returns what Collector.delete returns.
The collector is also given its origin, the object the delete began from: an instance here, a queryset when a queryset is deleted. It does nothing with it but pass it to the receivers of pre_delete and post_delete, so a receiver called for a manifest is told that it was a voyage that was deleted.
What comes back is a pair: the number of rows deleted, and a dictionary of that number by model, under each model’s label. Model.adelete awaits delete, wrapped in sync_to_async, with the same arguments.
What a collector holds
A Collector is made for one delete on one database. Between Collector and Collector.delete everything it knows is in these attributes:
| attribute | what it holds | filled by | what Collector.delete does with it |
|---|---|---|---|
Collector.data |
what it holds the instances to delete, by model class | filled by Collector.add |
what Collector.delete does with it deletes them by key, one model at a time, and sends the signals for them |
Collector |
what it holds querysets whose rows are to be deleted without having been fetched | filled by Collector |
what Collector.delete does with it sends one "DELETE" for each, with the queryset’s own condition |
Collector |
what it holds by field and value, the rows whose foreign key is to be given that value | filled by Collector |
what Collector.delete does with it updates those rows, one field and value at a time |
Collector |
what it holds by model and field, the rows that RESTRICT found |
filled by Collector |
what Collector.delete does with it nothing: collect settles them before it returns |
Collector |
what it holds for a model, the models whose rows have to be deleted before its own | filled by Collector |
what Collector.delete does with it puts the models of data in order by it |
An instance in data is one Django holds: it can send a signal about it, and can use its key to look for the rows that point at it. A queryset in fast_deletes stands for rows nobody has seen.
Collector.collect
Collector is given a collection of instances of one model: the list that Model.delete made, a queryset, or the rows a relation’s function hands back to it. With each it does the same things in the same order.
First, if Collector accepts what was given, it goes into Collector as it stands and the method returns. A list is never accepted, so the call that Model.delete makes always goes on; a queryset may be.
Then Collector.add enters the instances in Collector.data under the class of the first of them, and returns those that were not there before. When none is new, collect returns, which is also what ends the recursion where relations lead back to rows already collected.
For a child with a table of its own, the parent’s row of each new instance is collected too, by a call of collect that is told not to follow relations, unless this call was told to keep parents. The docstring of collect calls this the one case in which a cascade follows a foreign key forwards.
The relations come next. get_candidate_relations_to_delete picks, from what Options lists for the model, the entries that were made automatically, have no column, and are one-to-one or one-to-many. Among Django’s own classes only a rel answers to that: the far end of a foreign key or a one-to-one field that points at this model, whichever model declares it. A child with a table of its own has the relations that point at its parents too: an Officer’s list has those that point at Sailor, "captain" of Ship among them, and not its own "sailor_ptr". A model’s own relation fields are not in the list: they point away from it, and deleting a ship does nothing to its port.
A many-to-many rel is left out, and the rows of such a relation are reached all the same, because its through model has two foreign keys of its own (Many-to-many relations). Where Django made that model, both keys have a related_name ending in "+", which makes a relation hidden, and get_candidate_relations_to_delete asks for the hidden ones too (The options: what a model keeps as _meta).
For each relation collect reads the on_delete its field was declared with, kept on the rel as ForeignObjectRel (Relations: a field, its rel and the class at the other end), and does one of three things.
It passes the relation over when the value is in SKIP_COLLECTION: DO_NOTHING, and DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT, which leave the rows to the database. Only a collector made with force_collection=True does otherwise.
It marks the relation for a fast delete when can_fast_delete accepts the related model. Once every relation has been gone through, each model so marked gives a queryset of its rows that point at the new instances, which goes into fast_deletes unfetched.
Otherwise it gets the rows and calls the relation’s on_delete with them. Collector makes the queryset: the related model’s base manager, not its default one, filtered to the rows whose field is one of the instances (Managers). The function is called with the collector, the field, the queryset and the alias, and for most functions only if the queryset has rows, which is found out by fetching it. The fetch is as spare as will do: where no receiver of pre_delete or post_delete is connected for the related model, the queryset is as a rule narrowed with QuerySet.only to the key and the fields that relations to that model refer to.
After the relations, a private field with an attribute named "bulk_related_objects", which in Django’s own source is a GenericRelation, is asked for the rows that point at the new instances through it, and those are collected by another call of collect (Generic relations: GenericRelation and generic inlines). Last, in the outermost call only, the rows that RESTRICT found are settled.
The on_delete functions
Apart from the three that leave the rows to the database, the values on_delete takes are ordinary functions of django/, each called with the collector, the field, the rows and the alias. What a relation does on a delete is whatever its function does to the collector with them, and ForeignKey asks of the value only that it can be called.
on_delete.| function | what it does with the rows it is handed |
|---|---|
CASCADE |
calls collect on them, naming the model the field points at as the source |
PROTECT |
raises ProtectedError, which carries them |
RESTRICT |
files them with Collector, and adds a dependency between the two models |
SET_NULL |
calls Collector with None |
SET_DEFAULT |
calls add_field_update with the field’s default |
SET |
is not such a function but returns one, which calls add_field_update with the value SET was given or, where that value is callable, with what calling it returns |
DO_NOTHING |
nothing, and collect does not call it: it is in SKIP_COLLECTION |
CASCADE is collect calling itself by way of the relation. The nested call is told the model the relation points at, and add makes a dependency of it: these rows are to be deleted before that model’s. Where the field may be null add makes none. The source’s comment there gives the reason: such relations are nulled out before deleting, and so do not affect the order. CASCADE itself schedules that update only on a backend whose BaseDatabaseFeatures is false.
SET_NULL, and the function SET returns for a value that is not callable, carry a true attribute "lazy_sub_objs": collect calls them without fetching the rows to see whether there are any, and the queryset they are handed, still unfetched when Collector.delete comes to it, becomes the condition of one "UPDATE".
ProtectedError and RestrictedError
ProtectedError and RestrictedError are subclasses of IntegrityError, each made with a message and the rows that stood in the way, kept as ProtectedError and RestrictedError.
A port has ships that call it home, through "home". Three more models point at a port, with CASCADE: Voyage_calls; Licence, the model between Sailor and Port, with a foreign key to each; and Berth, a model of the office whose key is a port and a number. In collect for such a port the error is raised at the first relation and leaves only after the last:
Collector.can_fast_delete(the class Ship, from_field=Ship.home) -> False
Collector.related_objects(Ship, [Ship.home], [<Port: Bergen>]) -> a QuerySet of Ship, not yet fetched
SQL SELECT "fleet_ship"."id" FROM "fleet_ship" WHERE "fleet_ship"."home_id" IN (%s) ORDER BY "fleet_ship"."name" ASC with params (1,)
PROTECT(collector, Ship.home, a QuerySet of Ship, fetched, 2 rows, 'default') raises ProtectedError
Collector.can_fast_delete(the class Voyage_calls, from_field=Voyage_calls.port) -> True
Collector.can_fast_delete(the class Licence, from_field=Licence.port) -> True
Collector.can_fast_delete(the class Berth, from_field=Berth.port) -> True
raises ProtectedError
the caller catches ProtectedError: Cannot delete some instances of model 'Port' because they are referenced through protected foreign keys: 'Ship.home'.
its protected_objects are Ships with the keys [1, 3]
collect caught the exception that PROTECT raised, kept its rows under the name of the relation it was working through, and went on; after the last relation it raised, once, an error that names each relation under which a ProtectedError had arrived, here the one protecting field, and carries the rows of them all. One statement was sent, a "SELECT" of the ships’ keys.
RESTRICT raises nothing when it is called. It files the rows in Collector and adds a dependency. Whether they stand in the way is not yet known: a restricted row may itself be on its way out, reached by a cascade through another of its relations. So the question is put at the end of the outermost call, the one Model.delete or QuerySet.delete makes. The calls nested in it, which CASCADE makes and which collect makes for a parent or a generic relation, pass fail_on_restricted=False and skip it.
There, each restricted row that is also in data is struck out. Each queryset in fast_deletes whose model has restricted rows is asked, by a query, which of them it would delete, and those are struck out too. If any row is left, RestrictedError is raised. A row behind PROTECT stops a delete whatever else is being deleted; a row behind RESTRICT stops it only if that row would be left behind.
A ship is deleted
Deleting one ship of the harbour takes Collector two levels down, to rows that are collected, rows that are marked for a fast delete and rows that are only to be updated. The Petrel has a voyage and five sailors, and this is what collect did with it:
Collector.collect([<Ship: Petrel>])
Collector.can_fast_delete([<Ship: Petrel>]) -> False
Collector.add([<Ship: Petrel>]) -> not already collected: [<Ship: Petrel>]
Collector.can_fast_delete(the class Voyage, from_field=Voyage.ship) -> False
Collector.related_objects(Voyage, [Voyage.ship], [<Ship: Petrel>]) -> a QuerySet of Voyage, not yet fetched
SQL SELECT "fleet_voyage"."id" FROM "fleet_voyage" WHERE "fleet_voyage"."ship_id" IN (%s) with params (1,)
CASCADE(collector, Voyage.ship, a QuerySet of Voyage, fetched, 1 rows, 'default')
Collector.collect(a QuerySet of Voyage, fetched, 1 rows, source=Ship, fail_on_restricted=False)
Collector.can_fast_delete(a QuerySet of Voyage, fetched, 1 rows) -> False
Collector.add(a QuerySet of Voyage, fetched, 1 rows) -> not already collected: [<Voyage: Voyage object (1)>]
Collector.can_fast_delete(the class Voyage_calls, from_field=Voyage_calls.voyage) -> True
Collector.can_fast_delete(the class Manifest, from_field=Manifest.voyage) -> True
Collector.related_objects(Voyage_calls, [Voyage_calls.voyage], [<Voyage: Voyage object (1)>]) -> a QuerySet of Voyage_calls, not yet fetched
Collector.related_objects(Manifest, [Manifest.voyage], [<Voyage: Voyage object (1)>]) -> a QuerySet of Manifest, not yet fetched
Collector.can_fast_delete(the class Sailor, from_field=Sailor.ship) -> False
Collector.related_objects(Sailor, [Sailor.ship], [<Ship: Petrel>]) -> a QuerySet of Sailor, not yet fetched
SQL SELECT "crew_sailor"."id" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" IN (%s) with params (1,)
CASCADE(collector, Sailor.ship, a QuerySet of Sailor, fetched, 5 rows, 'default')
Collector.collect(a QuerySet of Sailor, fetched, 5 rows, source=Ship, fail_on_restricted=False)
Collector.can_fast_delete(a QuerySet of Sailor, fetched, 5 rows) -> False
Collector.add(a QuerySet of Sailor, fetched, 5 rows) -> not already collected: [<Sailor: pk=1>, <Sailor: pk=2>, <Sailor: pk=4>, <Sailor: pk=5>, <Sailor: pk=7>]
Collector.can_fast_delete(the class Ship, from_field=Ship.captain) -> False
Collector.related_objects(Ship, [Ship.captain], [<Sailor: pk=1>, <Sailor: pk=2>, <Sailor: pk=4>, <Sailor: pk=5>, <Sailor: pk=7>]) -> a QuerySet of Ship, not yet fetched
SET_NULL(collector, Ship.captain, a QuerySet of Ship, not yet fetched, 'default')
Collector.add_field_update(Ship.captain, None, a QuerySet of Ship, not yet fetched)
Collector.can_fast_delete(the class Officer, from_field=Officer.sailor_ptr) -> False
Collector.related_objects(Officer, [Officer.sailor_ptr], [<Sailor: pk=1>, <Sailor: pk=2>, <Sailor: pk=4>, <Sailor: pk=5>, <Sailor: pk=7>]) -> a QuerySet of Officer, not yet fetched
SQL SELECT "crew_sailor"."id", "crew_officer"."sailor_ptr_id" FROM "crew_officer" INNER JOIN "crew_sailor" ON ("crew_officer"."sailor_ptr_id" = "crew_sailor"."id") WHERE "crew_officer"."sailor_ptr_id" IN (%s, %s, %s, %s, %s) with params (1, 2, 4, 5, 7)
Collector.can_fast_delete(the class Licence, from_field=Licence.sailor) -> True
Collector.related_objects(Licence, [Licence.sailor], [<Sailor: pk=1>, <Sailor: pk=2>, <Sailor: pk=4>, <Sailor: pk=5>, <Sailor: pk=7>]) -> a QuerySet of Licence, not yet fetched
Two relations point at Ship, and neither could be marked for a fast delete, because rows of further models point at a voyage and at a sailor, which is one of the things that rule a fast delete out. So the ship’s voyages were fetched, as keys and nothing more, and CASCADE handed them back to collect, which found the two models that point at Voyage and marked both.
The five sailors went the same way, with three relations behind them. The one-to-one field "captain" is SET_NULL: its function was called with a queryset that was never fetched, and left a field update. "sailor_ptr" of Officer could not be marked, since an Officer inherits the relations that point at Sailor and they are not ones to pass over; its rows were fetched and there were none, so CASCADE was not called. And Licence was marked.
collect left the collector holding a ship, a voyage and five sailors in Collector.data, three querysets in Collector, one field update, and the dependencies that Voyage and Sailor go before Ship. What Collector.delete then sent is under The general case.
Collector.can_fast_delete : whether rows can go unseen
Collector is the test of whether rows can be deleted by one statement without being fetched. A no does not by itself mean a fetch. For a relation, a no hands the rows over to the relation’s function, and SET_NULL takes them unfetched. For rows that CASCADE is to delete a no does mean a fetch, and outside a forced collector it is given for one of two reasons: somebody is to be told of each row, or the rows’ keys are needed to find what else must go with them. The method says no when any of these holds:
- the collector was made with
force_collection=True; - the question comes on behalf of a relation, and the relation’s
on_deleteis notCASCADEitself; - what it is asked about is neither a model class, nor an instance, nor a queryset: a list, for one;
- a receiver is connected to
pre_deleteor topost_deletefor the model, a receiver connected for every sender included; - the model has a parent with a table, unless the question comes by the model’s link to that parent and it has no other;
- some relation that points at the model has an
on_deleteoutsideSKIP_COLLECTION, so that its rows would have to be dealt with in turn; - the model has a private field with an attribute
"bulk_related_objects", a generic relation.
The exception for a link to a parent is for a delete that arrives at a child from its parent’s row: the source’s comment says it avoids cascading back to the parent. Collector asks the question of the collection it was given and of each related model, with the relation’s field; Collector.delete asks it of the instance, when one instance is all there is.
The condition about receivers makes the queries of a delete depend on who is listening. With a receiver of pre_delete and one of post_delete connected for Manifest, the answer for Manifest is no, and a voyage’s manifests are fetched whole and get both signals:
Collector.can_fast_delete(the class Manifest, from_field=Manifest.voyage) -> False
SQL SELECT "office_manifest"."serial", "office_manifest"."voyage_id", "office_manifest"."crates", "office_manifest"."kilos_each", "office_manifest"."kilos", "office_manifest"."stamped", "office_manifest"."scan" FROM "office_manifest" WHERE "office_manifest"."voyage_id" IN (%s) with params (3,)
CASCADE(collector, Manifest.voyage, a QuerySet of Manifest, fetched, 2 rows, 'default')
Collector.collect(a QuerySet of Manifest, fetched, 2 rows, source=Voyage, fail_on_restricted=False)
Collector.can_fast_delete(a QuerySet of Manifest, fetched, 2 rows) -> False
Collector.add(a QuerySet of Manifest, fetched, 2 rows) -> not already collected: [<Manifest: Manifest object (M-0003)>, <Manifest: Manifest object (M-0004)>]
...
signal pre_delete, sender Manifest, instance=<Manifest: Manifest object (M-0003)>
signal pre_delete, sender Manifest, instance=<Manifest: Manifest object (M-0004)>
DeleteQuery.delete_batch(['M-0004', 'M-0003']) (Manifest)
SQL DELETE FROM "office_manifest" WHERE "office_manifest"."serial" IN (%s, %s) with params ('M-0004', 'M-0003')
signal post_delete, sender Manifest, instance=<Manifest: Manifest object (M-0004)>
signal post_delete, sender Manifest, instance=<Manifest: Manifest object (M-0003)>
Left out are the line that made the queryset and the fast delete of the ports of call, for whose model nothing was connected. How a signal finds its receivers is in Signals: Signal, receivers and sending.
Collector.delete sends neither signal for a model whose options have Options set, as the through model that Django makes for a many-to-many field has. can_fast_delete makes no such exception. A receiver connected for such a model costs the fetch and is not called for its rows.
An on_delete left to the database
A database can cascade a delete without help: a foreign key’s constraint may carry a clause such as ON DELETE CASCADE. DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT are the values of on_delete that ask for one, and the office’s logbook uses the first:
# harbour/office/models.py
class Logbook(models.Model):
title = models.CharField(max_length=60)
class LogEntry(models.Model):
book = models.ForeignKey(Logbook, on_delete=models.DB_CASCADE)
line = models.CharField(max_length=200)
Each of the three is an instance of DatabaseOnDelete, made with the words of its clause, and the schema editor has the clause from its DatabaseOnDelete. In two of the harbour’s tables as SQLite’s schema editor made them, a sailor’s foreign key to its ship was declared with CASCADE and an entry’s to its logbook with DB_CASCADE, and only the second constraint says anything about a delete:
editor.create_model(Sailor)
SQL CREATE TABLE "crew_sailor" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(60) NOT NULL, "ship_id" bigint NOT NULL REFERENCES "fleet_ship" ("id") DEFERRABLE INITIALLY DEFERRED, "signed_on" datetime NOT NULL)
...
editor.create_model(LogEntry)
SQL CREATE TABLE "office_logentry" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "book_id" bigint NOT NULL REFERENCES "office_logbook" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "line" varchar(200) NOT NULL)
The collector leaves a relation declared with one of the three alone, as it leaves one declared with DO_NOTHING: they are in SKIP_COLLECTION with it. A logbook with two entries is deleted with one statement, and the entries are gone:
LogEntry.objects.count() -> 2
...
Collector.can_fast_delete(<Logbook: Logbook object (1)>) -> True
mark_for_rollback_on_error
DeleteQuery.delete_batch([1]) (Logbook)
SQL DELETE FROM "office_logbook" WHERE "office_logbook"."id" IN (%s) with params (1,)
-> (1, {'office.Logbook': 1})
...
LogEntry.objects.count() -> 0
Collector, whose lines are left out, added the logbook and followed no relation. Django knew nothing of the two entries: no row was fetched, no receiver of the delete signals for LogEntry would have been called, and the count returned is one.
A row that the database deletes is never collected, and nothing Django does for a collected row is done for it: no signal is sent, and no relation that points at it is followed. The model checks refuse a project in which the two kinds meet (The model checks).
A collector made with force_collection=True marks nothing for a fast delete, and follows a relation declared with DB_CASCADE by calling CASCADE, which that value carries as its DatabaseOnDelete. Django’s own source makes such collectors to find out what a delete would reach: the admin’s NestedObjects, from which it draws its page of confirmation (Deleting: delete_view and get_deleted_objects), and the command that removes stale content types.
Collector.delete
Collector.delete begins by putting things in order. The instances of each model are sorted by key. Then Collector.sort tries to arrange the models so that each comes after every model it depends on: it goes round them, taking each whose dependencies have all been taken, and if a round places none it gives up and leaves the order as it was.
The dependencies are those the collector was told of while it collected: by Collector.add, for rows a cascade reached through a field that cannot be null; the other way round for a parent’s row; and by RESTRICT. The source’s comment says whom the order is for: databases that have no transactions, or cannot defer the checking of constraints to the end of one.
One instance and nothing else
Collector.delete takes a short way where Collector.data holds one instance of one model and Collector accepts that instance: one "DELETE" by key and no signal, the test having just established that there is no receiver for one. The statement is sent inside mark_for_rollback_on_error, which opens no transaction; its docstring says it is there to avoid starting one where a single query is executed in autocommit mode. A licence is such a row, since nothing points at a licence:
Collector.can_fast_delete([<Licence: Licence object (2)>]) -> False
Collector.add([<Licence: Licence object (2)>]) -> not already collected: [<Licence: Licence object (2)>]
Collector.delete
Collector.sort
Collector.can_fast_delete(<Licence: Licence object (2)>) -> True
mark_for_rollback_on_error
DeleteQuery.delete_batch([2]) (Licence)
SQL DELETE FROM "office_licence" WHERE "office_licence"."id" IN (%s) with params (2,)
-> (1, {'office.Licence': 1})
Afterwards:
licence.pk, licence._state.adding, Licence.objects.filter(port=leith).exists() -> (None, False, False)
The attribute under the attname of the key has been set to None, and nothing else on the instance is touched: its state still says that it is not being added (An instance and its state).
The general case
In every other case Collector.delete works inside a block of atomic made with savepoint=False, which begins a transaction when none is open and otherwise joins the one that is. Inside it, in this order:
pre_deleteis sent for every instance indata, with the instance’s class as the sender, unless the model is auto-created.- Each queryset in
Collectoris given to.fast_deletes QuerySet, which sends one._raw_delete "DELETE"with the queryset’s condition and returns the number of rows. - The field updates are made. For each field and value, the querysets that were never fetched are joined into one and updated with
QuerySet.update, and the rows that were fetched are updated by key withUpdateQuery..update_batch - The models are taken in their sorted order. For each, the instances are deleted by key with
DeleteQuery, highest key first, and then.delete_batch post_deleteis sent for each of them, again unless the model is auto-created.
So every pre_delete is sent before any row is changed, and a post_delete is sent inside the transaction, while the rows of models later in the order are still there. This is that block for the Petrel, the ship whose collecting is followed above: three fast deletes, one update, then the three models by key, Ship last.
Collector.delete
Collector.sort
Atomic.__enter__ (savepoint=False)
SQL BEGIN
QuerySet._raw_delete (a QuerySet of Voyage_calls, not yet fetched)
SQL DELETE FROM "fleet_voyage_calls" WHERE "fleet_voyage_calls"."voyage_id" IN (%s) with params (1,)
QuerySet._raw_delete (a QuerySet of Manifest, not yet fetched)
SQL DELETE FROM "office_manifest" WHERE "office_manifest"."voyage_id" IN (%s) with params (1,)
QuerySet._raw_delete (a QuerySet of Licence, not yet fetched)
SQL DELETE FROM "office_licence" WHERE "office_licence"."sailor_id" IN (%s, %s, %s, %s, %s) with params (1, 2, 4, 5, 7)
mark_for_rollback_on_error
SQL UPDATE "fleet_ship" SET "captain_id" = %s WHERE "fleet_ship"."captain_id" IN (%s, %s, %s, %s, %s) with params (None, 1, 2, 4, 5, 7)
DeleteQuery.delete_batch([1]) (Voyage)
SQL DELETE FROM "fleet_voyage" WHERE "fleet_voyage"."id" IN (%s) with params (1,)
DeleteQuery.delete_batch([7, 5, 4, 2, 1]) (Sailor)
SQL DELETE FROM "crew_sailor" WHERE "crew_sailor"."id" IN (%s, %s, %s, %s, %s) with params (7, 5, 4, 2, 1)
DeleteQuery.delete_batch([1]) (Ship)
SQL DELETE FROM "fleet_ship" WHERE "fleet_ship"."id" IN (%s) with params (1,)
Atomic.__exit__
BaseDatabaseWrapper.commit
-> (13, {'fleet.Voyage_calls': 3, 'office.Manifest': 2, 'office.Licence': 1, 'fleet.Voyage': 1, 'crew.Sailor': 5, 'fleet.Ship': 1})
Collector.sort put Voyage and Sailor ahead of Ship. The update is a statement of QuerySet.update, which brings that method’s own mark_for_rollback_on_error with it. No receiver was connected, so there is no line for a signal. The total of 13 is the rows the "DELETE" statements removed; what an "UPDATE" changes is not counted.
After the block, the attribute under the key’s attname is set to None on every instance in data, and the method returns the pair that Model.delete returns. The counts are what the "DELETE" statements reported, added up by model, and on this path a model whose statements removed no row has no entry.
A model with a parent, and keep_parents=True
An Officer is a row in each of two tables with one key (Inheritance: abstract bases, parents with tables and proxies), and deleting one deletes both rows unless the delete is told otherwise. In Collector, once the instances have been added, the parent’s instance of each is read off the child through its link (Reading and setting related objects), and the parents are handed to collect with three instructions. It is not to follow relations: those that point at the parent are already in the child’s own list, as the source’s comment says. It is to record the dependency the other way round, since here the row being collected is the one pointed at and the child’s row has to go first. And it is not to settle restricted rows.
With keep_parents=True the parents are not collected, and a relation that points at one of the model’s parents is passed over, so the rows that point at the parent’s row are left pointing at it. An Officer deleted so loses one row, the child’s, with nothing followed:
Collector.add([<Officer: Finn>]) -> not already collected: [<Officer: Finn>]
Collector.delete
Collector.sort
Collector.can_fast_delete(<Officer: Finn>) -> False
Atomic.__enter__ (savepoint=False)
SQL BEGIN
DeleteQuery.delete_batch([6]) (Officer)
SQL DELETE FROM "crew_officer" WHERE "crew_officer"."sailor_ptr_id" IN (%s) with params (6,)
...
Officer.objects.filter(pk=6).exists(), Sailor.objects.filter(pk=6).exists() -> (False, True)
One instance was all the collector held, yet the short way was not taken: Collector says no to a model with a parent, and the single statement got a transaction.
QuerySet.delete
QuerySet.delete uses the same collector, given a queryset where Model.delete gives a list of one instance. The difference shows at the first step of Collector, where a queryset, unlike a list, can be accepted for a fast delete. The berths of a port are accepted, and a queryset of sailors is not:
Collector.can_fast_delete(a QuerySet of Berth, not yet fetched) -> True
Collector.delete
Collector.sort
Atomic.__enter__ (savepoint=False)
SQL BEGIN
QuerySet._raw_delete (a QuerySet of Berth, not yet fetched)
SQL DELETE FROM "office_berth" WHERE "office_berth"."port_id" = %s with params (1,)
...
Collector.can_fast_delete(a SailorQuerySet of Sailor, not yet fetched) -> False
Collector.add(a SailorQuerySet of Sailor, not yet fetched)
SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" WHERE "crew_sailor"."name" = %s with params ('Finn',)
-> not already collected: [<Sailor: Finn>]
The berths went in one statement whose condition is the queryset’s own, though still in a transaction. Rows of other models point at a sailor, so Collector.add fetched that queryset, whole rows as the caller’s queryset asks for them, and the delete went on as it does for an instance. What QuerySet.delete does around the collector belongs to QuerySets.
What a delete leaves alone
Model.delete runs for the instance it is called on. The rows a collector reaches are deleted by the collector’s own statements, and no delete is called on them, a model’s override included; what reaches them is the two signals, and those only the rows it holds as instances.
A file is not deleted with its row: nothing in a delete touches a file field’s storage (File fields).