Removing model abstractions, changing field options
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
OneToOneField
- Requiring
through
onManyToManyField
- Removed
db_index
fields - Removed
unique
fields - Changed
blank
fields torequired
- Changed
null
fields toallow_null
- Other notable changes
- Next steps
Removed abstract
models, proxy
models, and multi-table inheritance
Django has several ways that you can do 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!
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.
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.
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, 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.