Up to this point, I haven't made that many changes to models. But there are some things I've been considering for quite a while and finally decided to tackle. The biggest changes involve removing abstractions that paper over how the database is actually being implemented. "Magic" is a bit of an overused and undefined term, but I think it applies here. Plain strives to be more straightforward and obvious — if the code doesn't clearly reflect what is happening in the database, then it *may* be too abstract. The tradeoff is that we push more code and knowledge onto the developer, but in the long run I think this is a good thing for beginners and experts alike. I'm sure some people would disagree with these changes, but the nice thing about *removing* features is that we can always add them back! The only person that has to be convinced is me. Here are the changes: - [Removed `abstract` models, `proxy` models, and multi-table inheritance](#removed-abstract-models-proxy-models-and-multi-table-inheritance) - [Removed `OneToOneField`](#removed-onetoonefield) - [Requiring `through` on `ManyToManyField`](#requiring-through-on-manytomanyfield) - [Removed `db_index` fields](#removed-db_index-fields) - [Removed `unique` fields](#removed-unique-fields) - [Changed `blank` fields to `required`](#changed-blank-fields-to-required) - [Changed `null` fields to `allow_null`](#changed-null-fields-to-allow_null) - [Other notable changes](#other-notable-changes) - [Next steps](#next-steps) ## Removed `abstract` models, `proxy` models, and multi-table inheritance Django has several ways that you can do [model inheritance](https://docs.djangoproject.com/en/5.1/topics/db/models/#model-inheritance). I'm not going to go over them in detail here, but odds are if you don't know them, then you probably don't need to! Personally, I don't think they're worth the complexity — I prefer to have my models map pretty directly to the database structure, so I've made some changes in that direction. Now when you create a model in Plain, it has to inherit *directly* from `models.Model`. It can't extend another model subclass. For the vast majority of use cases, nothing changes! ```python from plain import models @register_model class User(models.Model): email = models.EmailField() ``` We do still support the *purpose* of `abstract` models, but they are ordinary classes instead of `models.Model` with `Meta.abstract = True`. You can think of these more as "mixins", where you simply group some fields that you want to reuse across your app. ```python from plain import models class TimestampedModel: created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @register_model class User(TimestampedModel, models.Model): email = models.EmailField() ``` So you can still reuse fields and class methods, but there is no automatic reuse of `Meta` (which was too confusing anyway, in my opinion). ## Removed `OneToOneField` The `OneToOneField` was basically just a `ForeignKey` with `unique=True`. Again, if we're being more transparent about what is *actually* happening then I think it is ok to use a `ForeignKey` with a `UniqueConstraint`. Otherwise it can be hard to envision how this is working in the database. I will admit that it was kind of nice to have the reverse accessor set up as a single object instead of a queryset, but again I think it's ok if the framework leans more towards clarity of what is happening in the database. We'll see! ## Requiring `through` on `ManyToManyField` The `ManyToManyField` now requires that you define a `through` model to go with it. It will no longer create one for you at runtime (which was also the only instance of an "auto created" model, which is a concept we can now remove entirely). In my experience, you almost always end up wanting control of this model anyway so you can attach additional data to the relationship. ```python from plain import models @register_model class User(models.Model): email = models.EmailField() @register_model class Group(models.Model): name = models.CharField(max_length=100) members = models.ManyToManyField(User, through="GroupMembership") @register_model class GroupMembership(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE) date_joined = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint(fields=["user", "group"], name="unique_user_group_membership") ] ``` ## Removed `db_index` fields Up to this point you could define `Meta.indexes` for the entire model, or you could index a single field with `db_index=True`. The Django docs say that `db_index` [may be deprecated in the future](https://docs.djangoproject.com/en/5.1/ref/models/fields/#db-index), but we can skip straight to removing it. `Meta.indexes` are way more flexible and just forcing you to define indexes at the model level typically leads to better decisions anyway. The exception to this change is `ForeignKey` fields. Not every database indexes foreign keys, and some databases allow you to *disable* the index. Long story short, `ForeignKey` fields have their own `db_index` parameter that defaults to `True`, giving you the option to set it to `False`. Otherwise there's no way to remove an index from a foreign key. ## Removed `unique` fields Like `db_index`, database uniqueness could be enforced per field with `unique=True`, or at the model level with `Meta.constraints`. Again, I think doing this at the model level is more flexible and leads to more holistic, better decisions. So `unique` is gone and you should use `Meta.constraints` instead! ## Changed `blank` fields to `required` I've always found `blank=True` to be confusing. Since this is really a validation rule, I think it makes more sense to say `required=False`. ## Changed `null` fields to `allow_null` For sake of clarity, `null` has been changed to `allow_null`. ## Other notable changes - Removed `Meta.order_with_respect_to` - Removed `SlugField` - Removed `editable` fields - Field keyword arguments are now required ## Next steps It's time to get serious about documentation! Shifting into documentation will force me through each module and package to see where things stand. I'm sure some changes will come as a result of that process (being forced to explain in writing is a great way to find shortcomings), but the goal is to refocus on the user-facing content so we can push towards a 1.0 release.