2009-02-19

Abstract Models and Dynamicly Assigned Foreign Keys

Model inheritance enables creating extensible apps. You can define a reusable core app which includes base.py with abstract models and models.py with models extending the abstract ones and inheriting all the features. In the specific project you can either use the core app directly, or create a specific app which models extend from the base abstract models of the core app and additionally introduce new features.

This is a quick example skipping all the unrelated parts like settings, urls, and templates:


  • core_project
    • apps
      • player
        • base.py
          from django.db impport models
          class PlayerBase(models.Model):
          name = models.CharField(max_length=100)
          class Meta:
          abstract = True

        • models.py
          from core_project.apps.player.base import PlayerBase
          class Player(PlayerBase):
          pass


  • specific_project

    • apps

      • player

        • models.py
          from core_project.apps.player.base import PlayerBase
          class Player(PlayerBase):
          points = models.IntegerField()



The concept works fine until you need to use foreign keys or many-to-many relations in the abstract models. As Josh Smeaton has already noticed, you can't set foreign keys to abstract models as they have no own database tables and they know nothing about the models which will extend them.

Let's say, we have the following situation: GameBase and MissionBase are abstract models and the model extending MissionBase should receive a foreign key to the model extending GameBase.



Thanks to Pro Django book by Marty Alchin, I understood how the models get created in the background. By default, all python classes are constructed by the type class. But whenever you use __metaclass__ property for your classes, you can define a different constructor. Django models are classes constructed by ModelBase class which extends the type class.

In order to solve the problem of foreign keys to the models extending the abstract classes, we can have a custom constructor extending the ModelBase class.

base.py
# -*- coding: utf-8 -*-
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.fields import FieldDoesNotExist

class GameMissionCreator(ModelBase):
"""
The model extending MissionBase should get a foreign key to the model extending GameBase
"""
GameModel = None
MissionModel = None
def __new__(cls, name, bases, attrs):
model = super(GameMissionCreator, cls).__new__(cls, name, bases, attrs)
for b in bases:
if b.__name__=="GameBase":
cls.GameModel = model
elif b.__name__=="MissionBase":
cls.MissionModel = model
if cls.GameModel and cls.MissionModel:
try:
cls.MissionModel._meta.get_field("game")
except FieldDoesNotExist:
cls.MissionModel.add_to_class(
"game",
models.ForeignKey(cls.GameModel),
)
return model

class GameBase(models.Model):
__metaclass__ = GameMissionCreator

title = models.CharField(max_length=100)
class Meta:
abstract = True

class MissionBase(models.Model):
__metaclass__ = GameMissionCreator

title = models.CharField(max_length=100)
class Meta:
abstract = True


models.py
# -*- coding: utf-8 -*-
from base import *

class Game(GameBase):
pass

class Mission(MissionBase):
pass


GameMissionCreator is a constructor of GameBase, MissionBase, Game, and Mission classes. When it creates a class extending GameBase, the game model is registered as a property. When it creates a class extending MissionBase, the mission model is registered as a property. When both models are registered, a foreign key is added dynamically from one model to the other.

One drawback of this constructor-class example is that if there are more than one classes extending GameBase or MissionBase, then the code won't function correctly.

Anyway, the example shown illustrates the possible solution and gives a direction for further development of the idea.

5 comments:

  1. Is there a workaround for the drawback you mention? I can't seem to find a way to do it...

    ReplyDelete
  2. I have the same problem but with only one parent ABC.

    Another class would contain a ForeignKey to the abstract class.

    I tried using your example to create a constructor to do so, but I still get "cannot dfine relation with absract class" error. Any idea why ?

    ReplyDelete
  3. The idea of the trick was that it didn't create a ForeignKey to an abstract class, but rather a ForeignKey to a leaf class which is created from an abstract class. The constructor checks if the class created is created from an abstract class or not. Probably you could even check the abstractness explicitly by model._meta.abstract. If you have multiple models created from the abstract class which will get ForeignKeys from other models, you need a way to define to which non-abstract class the ForeignKey is assigned. That might be done saving final non-abstract classes in a dictionary and referencing to them by names (let's say, defined in the settings).

    If this didn't help you, maybe you can send a link to pasted code snippet so that I could check it and maybe give some advices.

    ReplyDelete
  4. Recently I found out that all this can be achieved much simpler just telling the app name and model name in the foreign key definition, like this:

    class GameBase(models.Model):
        title = models.CharField(max_length=100)
        class Meta:
            abstract = True

    class MissionBase(models.Model):
        game = models.ForeignKey("game_app.Game")
        title = models.CharField(max_length=100)
        class Meta:
            abstract = True

    ReplyDelete
  5. I like the "Wish this site were powered by Django button". What I wish is that all blogspot sites had a "Click here for printable view" button on them. :(

    ReplyDelete