Django Generic Factory


April 2019


151 раз


У меня есть два вопроса, которые беспокоят меня. Я столкнулся реализацией, где какой-либо документ, связаны с разным уровнем геоданных и хотел бы завод для их создания. Давайте рассмотрим пример, как я думал, что это может работать:

from django.contrib.gis.db import models

class Country(models.Model):
    name = models.CharField(max_length=60)

class Region(models.Model):
    country = models.ForeignKey(Country, on_delete=models.PROTECT)
    name = models.CharField(max_length=60)

class Law(models.Model):
    text = models.CharField(max_length=60)

    class Meta:
        abstract = True

class CountryLaw(Law):
    country = models.ForeignKey(Country, on_delete=models.CASCADE)

class RegionLaw(Law):
    region = models.ForeignKey(Region, on_delete=models.CASCADE)

# Not sure this work but the idea is here
class LawManager(models.Manager):
    def create_law(text,geodata):
        if isinstance(geodata, Country):
            return CountryLaw(text=text, country=geodata)
        elif isinstance(geodata, Region)
            return RegionLaw(text=text, region=geodata)
            raise TypeError("Inapropriate geodata type")

Я хотел бы некоторый фабричный метод, потому что у меня есть некоторые работы, чтобы заполнить поля «Закон», который является общим для всех закона, но этот пример не показывает. Мой вопрос в следующем:

  • Есть ли лучший способ проектирования объектов права?
  • Будет ли работать такой менеджер? Как я могу получить к нему доступ?

Я поиск по Google и StackOverflow для ответа, но не знаю, что ключевое слово, чтобы использовать и нашли что-нибудь техника его подводит, которые могли бы помочь мне ..

Спасибо за вашу помощь !

1 ответы


Well there are several options here. The following "list" is not exhaustive, although it might perhaps provide a few ideas, and variants can be constructed based on these ideas.

Leaving the models like it is

In that case we thus model it like:

+---------+ 1      N +------------+
| Country |----------| CountryLaw |
+---------+          +------------+
    | 1
    | N
+---------+ 1      N +-----------+
| Region  |----------| RegionLaw |
+---------+          +-----------+

Here we thus constructed two Laws. Although we can of course superclass the two Laws, it means that each has its own type.

The advantage is that if the two have specific semantics, for example a CountryLaw should be handled quite differently from a RegionLaw, then that is easier to implement. Furthermore if a CountryLaw has specific fields, that are for instance not important for RegionLaws (or vice versa), then we avoid wasting diskspace on NULL values (or other placeholders).

The downside is that if we for example want to query for laws that are part of 'Germany', we have to this in two steps: query for the CountryLaws of germany, and query for the RegionLaws of all regions of Germany. This can also easily get out of hand if you also have SubRegions, Citys, etc.

Working with Proxy regions

Here we consider all Laws to be made for a specific Region, but the trick is that we construct a Region that acts like the entire country. So besides 'Saxony' and 'Bavaria', we use a virtual region 'Germany' that will represent the entire country.

We can then introduce a single Law model, that is attached to Region. If we frequently need to make the distinction between a region, and a country, we can add a field is_country that for example specifies if this is a "country proxy" or a real region:

| Country |
    | 1
    | N
+-------------------+ 1      N +-----+
|       Region      |----------| Law |
+-------------------+          +-----+
| is_country : bool |

The advantage is that we have only one Law object, and therefore the design is easier. Furthermore it is easy to query for the laws that map on a country (including or excluding regions).

The downside is that if country Laws and region Laws differ significantly, then this will result in an awful amount of checking (each time looking at whether the attached region is really a region, or a country), and furthermore it can result in a lot of unused fields. Furthermore if we enable more and more layers, we need to introduce more and more proxy objects. If we would for example would use three layers (Country, Region, SubRegion), then we need to construct for every country a "proxy" region (that contains a proxy subregion as well), and for every region, a proxy subregion. So if there are n countries and m regions, this would result in 2×n + m proxy objects, which also results in data duplication (we repeat the name of the country/region multiple times, and if later a country or region is renamed, it will result in some pain to update all those proxies).

Working with a tree-like structure

In case the number of levels can be large, or dynamic (in the sense that some "areas"* have subregions, whereas for others there is no region), or we want to handle all these levels in a uniform way, we could decide to use a tree-like structure.

Here we define a model, for example Area, an Area can have a parent, which is an Area as well, we can construct a tree-structure that way. For example something like:

class Area(models.Model):
    parent = ForeignKey('app.Area', on_delete=models.SET_NULL, related_name='children')

We can then attach to each Area zero, one or more laws. So the model looks like:

 N|  |1
+------+ 1      N +-----+
| Area |----------| Law |
+------+          +-----+

The advantage is that here we have one model for Country, Region, Subregion, etc. Furthermore we can implement a hierarchy exactly the way we want where for example some (small) countries have no Regions (like for example "city states" like Vatican City, Singapore, etc.). Furthermore the Law object will link to an "area"-like object. It is also easy to obtain the laws attached to a certain area.

A problem is however that it is hard to obtain all the Laws of a country, its regions, its subregions, etc. This can be handled however, for example by designing a many-to-many table that encodes the transitive closure of this tree structure: this many-to-many relation then will contain the links a country with all its regions, subregions, etc., but still that is not very elegant. It also means that all these Area instances are represented uniformly. Therefore if we want to add for example a list with official languages to the Area, all areas have "official languages", whereas probably most subregions simply would "inherit" the official language of their country.

Using a GenericForeignKey in a Law object (Django feature)

Django also has a special sort of relation called the GenericForeignKey [doc]. This may look like a great feature for problems like this, but I would really advice to avoid such relations as much as possible.

By default Django add an implicit primary key to every model: if the developer does not specify a field with primary_key=True. Django will automatically add an IntegerField that will specify an identifier as primary key. It is thus reasonable to assume that most models have an IntegerField as primary key (in fact it israther un-Django to specify another primary key). We can also generate a list that maps every model to an integer: we could for example say that 0 maps to User, 1 maps to Group, etc.

This means that most model instances can be identified by two integers: one integer that specifies the model, and one that specifies the primary key of the corresponding model. For example (0, 14) is the User with primary key 14 (given we use the "lookup table" defined in the paragraph above). This is a powerful concept: we can thus use two database columns to store these integers, and each time let Django fetch the object. This is the idea behind a GenericForeignKey. We can thus define a Law like:

class Law(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    area = GenericForeignKey('content_type', 'object_id')

So this means that now our Law stores two real fields: a content_type and an object_id, and if we query for some_law.area, Django will fetch an object that corresponds with these two integers.

We thus could use this to refer to a Country, Region, Subregion, which can be useful in case we can refer to a variety of models. But there are a lot of disadvantages. The main problem is that such relations are usually very cumbersome in the case we want to use them in a query. Indeed: say we want to join our Law model with the area field. Then with which table should we join? The Country, the Region, the SubRegion? What if one Law refers to a Country whereas the other refers to a Region? So typically we can not JOIN.

Furthermore the model itself does not guarantee that the GenericForeignKey will always refer to an "area"-like object. It could refer to another Law, to a User, a Criminal, etc. So as a result you are responsible to write sane logic that will always make sure the relations make sense. Although this looks easy, it can be hard to maintain good sane relations. Most databases can not check whether the foreign key refers to a valid object, since there is no FOREIGN KEY constraint, since the "target table" is unknown.

Although there are a lot of drawbacks, in some cases a GenericForeignKey can be an elegant solution to certain problems, but one has to be careful.

Although there is - as far as I know - no generally accepted way to specify such relations in a diagram, it could look like this:

| Country |
    | 1      .
    |          .
    | N          .
+---------+        . +-----+
| Region  |. . . . . | Law |
+---------+ 1      N +-----+