开发者

Django Model Field for Abstract Base Class

I've searched around stack overflow for an answer to this (probably simple) question, but most of the solutions I see seem overly complicated and hard to understand.

I have a model "Post" which is an abstract base class. Models "Announcement" and "Event" inherit from Post.

Right now I'm keeping related lists of Events and Announcements in other models. For instance, I have "removed_events" and "removed_announcements" fields in another model.

However, in my project, "removed_events" and "removed_announcements" are treated exactly the same way. There is no need to disambiguate between a "removed event" and a "removed announcement." In other words, a field keeping track of "removed_posts" would be sufficient.

I don't know how to (or perhaps can't) create a field "removed_pos开发者_Go百科ts," since Post is abstract. However, right now I feel like I'm repeating myself in the code (and having to do a lot of clutter-some checks to figure out whether the post I'm looking at is an event or an announcement and add it to the appropriate removed field).

What is the best option here? I could make Posts non-abstract, but Post objects themselves should never be created, and I don't think I can enforce this on a non-abstract object.

My understanding of databases is weak, but I'm under the impression that making Post non-abstract would complicate the database due to joins. Is this a big deal?

Finally, there are other fields in other models where I'd like to condense things that amount to an event_list and an announcement_list into a post_list, but those fields do need to be disambiguated. I could filter the post_list based on post type, but the call to filter() would be slower than being able to directly access the event and announcement lists separately, wouldn't it? Any suggestions here?

Thanks a ton for reading through this.


There are two kinds of model subclassing in Django - Abstract Base Classes; and Multi-Table inheritance.

Abstract Base Classes aren't ever used by themselves, and do not have a database table or any form of identification. They are simply a way of shortening code, by grouping sets of common fields in code, not in the database.

For example:

class Address(models.Model):
    street = ...
    city = ...

    class Meta:
        abstract = True


class Employee(Address):
    name = ...

class Employer(Address):
    employees = ...
    company_name = ...

This is a contrived example, but as you can see, an Employee isn't an Address, and neither is an Employer. They just both contain fields relating to an address. There are only two tables in this example; Employee, and Employer - and both of them contain all the fields of Address. An employer address can not be compared to an employee address at the database level - an address doesn't have a key of its own.

Now, with multi-table inheritance, (remove the abstract=True from Address), Address does have a table all to itself. This will result in 3 distinct tables; Address, Employer, and Employee. Both Employer and Employee will have a unique foreign key (OneToOneField) back to Address.

You can now refer to an Address without worrying about what type of address it is.

for address in Address.objects.all():
    try:
        print address.employer
    except Employer.DoesNotExist: # must have been an employee
        print address.employee

Each address will have its own primary key, which means it can be saved in a fourth table on its own:

class FakeAddresses(models.Model):
    address = models.ForeignKey(Address)
    note = ...

Multi-table Inheritance is what you're after, if you need to work with objects of type Post without worrying about what type of Post it is. There will be an overhead of a join if accessing any of the Post fields from the subclass; but the overhead will be minimal. It is a unique index join, which should be incredibly quick.

Just make sure, that if you need access to the Post, that you use select_related on the queryset.

Events.objects.select_related(depth=1)

That will avoid additional queries to fetch the parent data, but will result in the join occurring. So only use select related if you need the Post.

Two final notes; if a Post can be both an Announcement AND an Event, then you need to do the traditional thing, and link to Post via a ForeignKey. No subclassing will work in this case.

The last thing is that if the joins are performance critical between the parent and the children, you should use abstract inheritance; and use Generic Relations to refer to the abstract Posts from a table that is much less performance critical.

Generic Relations essentially store data like this:

class GenericRelation(models.Model):
    model = ...
    model_key = ...


DeletedPosts(models.Model):
    post = models.ForeignKey(GenericRelation)

That will be a lot more complicated to join in SQL (django helps you with that), but it will also be less performant than a simple OneToOne join. You should only need to go down this route if the OneToOne joins are severely harming performance of your application which is probably unlikely.


Generic relationships and foreign keys are your friend in your path to succeed. Define an intermediate model where one side is generic, then the other side will get a related list of polymorphic models. It's just a little more complicated than a standard m2m join model, in that the generic side has two columns, one to ContentType (actually a FK) and the other to the PK of the actual linked model instance. You can also restrict the models to be linked with using standard FK parameters. You'll get used with it quickly.

(now that I get an actual keyboard to write with, here there is the example:)

class Post(models.Model):
    class Meta: abstract = True
    CONCRETE_CLASSES = ('announcement', 'event',)
    removed_from = generic.GenericRelation('OwnerRemovedPost',
        content_type_field='content_type',
        object_id_field='post_id',
    )

class Announcement(Post): pass

class Event(Post): pass

class Owner(models.Model):

    # non-polymorphic m2m
    added_events = models.ManyToManyField(Event, null=True)

    # polymorphic m2m-like property
    def removed_posts(self):
        # can't use ManyToManyField with through.
        # can't return a QuerySet b/c it would be a union.
        return [i.post for i in self.removed_post_items.all()]

    def removed_events(self):
        # using Post's GenericRelation
        return Event.objects.filter(removed_from__owner=self)


class OwnerRemovedPost(models.Model):
    content_type = models.ForeignKey(ContentType,
        limit_choices_to={'name__in': Post.CONCRETE_CLASSES},
    )
    post_id = models.PositiveIntegerField()
    post = generic.GenericForeignKey('content_type', 'post_id')
    owner = models.ForeignKey(Owner, related_name='removed_post_items')

    class Meta:
        unique_together = (('content_type', 'post_id'),)  # to fake FK constraint

You can't filter into the related collection like a classic many-to-many, but with the proper methods in Owner, and using the concrete classes' managers smartly, you get everywhere you want.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜