The proposal:
Create
a plugin system where plugins can register handlers / callbacks to be
run from within the automigrator. Potentially this could even be
implemented using the signals mechanism currently in Django, as presented in 'Building plugin ecosystems with Django', by Raphael Michel at DjangoCon Europe 2019.
I imagine the signal should be fired here:
And that signals handlers should act as combination between the 'create_altered_X' and 'generate_altered_X' methods, resulting either to calls of 'self.add_operation', or returning a list of operations to add to the migrationsfile.
There is an open question with regards to how dependencies and shared state should be handled.
- I do not have enough insight to answer this.
I am very open to other solutions, this is merely what I came up with, without having any throughout insight into how Django works internally.
- I am willing to implement the plugin system, and create a PR for this, if a consensus is found.
My personal motivation (long section):I
have a database design, which includes several data invariants, which I
want to uphold. I've been able to implement most of these data
invariants using uniqueness (on fields, unique_together,
UniqueConstraint), and CheckConstraint.
CheckConstraint is excellent, and I'm very happy that made it to
Django with 2.2, but for enforcing data invariants it has one major shortcoming. - It only operates on a single table.
Currently
to implement data invariants across multiple tables, I believe the
suggestion is to override save methods, or to use save signals.
-
However these signals are not emitted for queryset operations, such as
bulk_inserts or update s, and as such cannot be used to maintain data
invariants, without relying on manual procedures that are error-prone,
and take up review time. On another note, save methods and save signals,
also do not help if the databsae is accseed outside of the
Django ORM.
Thus
if you need reliable constraints that work across multiple tables, I
don't believe that a robust solution is currently in place, thus I've
been working on a prototype for a library to fulfill this role. The
library enables very strong data invariant checking by allowing one to
write database constraint triggers via Querysets.
The envisioned library is very simple, and works as follows (based upon the pizza toppings example):
class Topping(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return self.name
class PizzaTopping(models.Model):
class Meta:
unique_together = ("pizza", "topping")
pizza = models.ForeignKey('Pizza')
topping = models.ForeignKey('Topping')
class Pizza(models.Model):
name = models.CharField(max_length=30)
toppings = models.ManyToManyField('Topping', through=PizzaTopping)
def __str__(self):
return self.name
We envision having a data invariant on the number of toppings allowed per pizza,
and with the library we can now enforce that, by adding
the 'constraint_triggers' option to the the Meta class of PizzaTopping:
The
way the library works right now, is by monkey patching the autodetector
to pick up the constraint_triggers entry on the meta class, and by
expanding the OPTIONS.DEFAULT_NAMES to allow for setting this variable
on the Meta class.
- The combination of these are then used to automatically generate entries in migrationfiles with custom operations.
The
fact that monkey-patching is used means this library is not
interoperable with any other libraries changing the autodetector like
this, and that code smells.