开发者

django: is there a black magic on extra fields on many-to-many relationships?

It's from Django's official doc: https://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships

The models are like:

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __unicode__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __unicode__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

In the end, it gives such an example:

# Find all the members of the Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(
...     group__name='The Beatles',
...     membership__date_joined__gt=date(1961,1,1))
[<Person: Ringo Starr]

My question is how Person model is aware of a group and membership attributes since they are not defined on it开发者_如何学Python. Is it just a magic of manytomany with through or it's some kind of universal one in Django?

If I were to achieve the same query, I would think following code more natural (from Django's orm perspective, not from business one):

Membership.objects.filter(group__name='The Beatles', date_joined__gt=date(1961,1,1))).select_related('person')

Edit I read the document again and do find such backwards is universal. In paragraph https://docs.djangoproject.com/en/dev/topics/db/queries/#lookups-that-span-relationships, it mentions:

It works backwards, too. To refer to a "reverse" relationship, just use the lowercase name of the model.

I've never used this backwards query before. So the first time I saw it, it banged my brain. Sorry that this thread turns out to be stupid. But I hope it helps people who skip the very (thin) line in that paragraph.


Its not a magic of ManyToMany fields; its a universal one of the django ORM with foreign key (including ManyToMany) relations.

E.g., if you have

class Musician(models.Model):
    name = models.CharField(max_length=128)
    band = models.ForeignKey("Band")

class Band(models.Model):
    name = models.CharField(max_length=128)
    genre = models.CharField(max_length=50)

You can do something like Musician.objects.filter(band__name='The Beatles') The query can be seen with django-debug-toolbar or from the django shell with:

from django.db import connection
connection.queries

and will be comrpised of a complicated JOIN statement. The SQL query constructed by the ORM will look something like:

SELECT "appname_musician"."id", "appname_musician"."name", "appname_musician"."band_id" 
    FROM "appname_musician" 
    INNER JOIN "appname_band" ON ("appname_musician"."band_id" = "appname_band"."id") 
    WHERE "appname_band"."name" = "The Beatles";

EDIT: If you did Band.objects.filter(musician__name = 'Ringo Starr') the ORM would translate it into:

SELECT "appname_band"."id", "appname_band"."name", "appname_band"."genre"
    FROM "appname_band" 
    INNER JOIN "appname_musician" ON ("appname_musician"."band_id" = "appname_band"."id") 
    WHERE "appname_musician"."name" = "Ringo Starr";

The ORM knows the relationships and can convert your ORM query into the appropriate SQL. It doesn't matter that Band doesn't have a musician object; you were able to give django a clear request: I want all band objects, where there's musician linked to the band with the name 'Ringo Starr'.

I think you may be hung up on thinking about object encapsulation (e.g., band doesn't have a musician; how can you filter based upon it)? Its not black magic, as the filter request is clear and unambiguous -- and takes as arguments not members of the Band object; but commands to filter by. E.g., you could do Band.objects(name__icontains = "The"), even though there's no name__icontains object in Band. The ORM converts your request into SQL, which is simple to execute quickly.


EDIT2: Your final example:

class Musician(models.Model):
    name = models.CharField(max_length=128)
    initial_band = models.ForeignKey("Band", related_name = 'initial_band_members')
    final_band = models.ForeignKey("Band", related_name = 'final_band_members')

class Band(models.Model):
    name = models.CharField(max_length=128)
    genre = models.CharField(max_length=50)

try: ## create some objects
    cream,_ = Band.objects.get_or_create(name="Cream", genre="Classic Rock")
    derek, _ = Band.objects.get_or_create(name="Derek and the Dominos", genre="Classic Rock")
    beatles, _ = Band.objects.get_or_create(name="The Beatles", genre="Classic Rock")
    wings, _ = Band.objects.get_or_create(name="Wings", genre="Classic Rock")
    Musician.objects.get_or_create(name="Eric Clapton", initial_band=cream, final_band=derek)
    Musician.objects.get_or_create(name="Paul McCartney", initial_band=beatles, final_band=wings)
    Musician.objects.get_or_create(name="John Lennon", initial_band=beatles, final_band=beatles)

except:
    pass

Then queries like Band.objects.filter(musician__name='Paul McCartney') don't work (FieldError), but queries like Band.objects.filter(initial_band_members__name='Paul McCartney') will give back The Beatles found with the following SQL (using sqlite as a db; the command is dependent on the DB backend):

SELECT "testing_band"."id", "testing_band"."name", "testing_band"."genre" 
  FROM "testing_band" 
  INNER JOIN "testing_musician" ON ("testing_band"."id" = "testing_musician"."final_band_id") 
  WHERE "testing_musician"."name" = Paul McCartney '


Perhaps look into https://docs.djangoproject.com/en/dev/topics/db/queries/ for your answer. In general joins are used.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜