23 January, 2009

ManyToMany relations in Django.

I didn't write anything for some time, since was very busy with my work and family problems.
But today I have some interesting thing about django, so I can't keep silence (I'm new to python/django, so it can be interesting for newbies only, but I hope it is not).

First of all I want to say sorry for indentation in quoting blocks... Will fix it later...

In django to define ManyToMany relation you should use ManyToManyField in your model. Consider folowing code:


class User(models.Model):
    #...
    groups = ManyToManyField('Group')
class Group(models.Model):
    #...


If you use admin or just manually create a form from the model, you will get a widget to edit M2M just for User. And it's normal since according to docs "By default, admin widgets for many-to-many relations will be displayed inline on whichever model contains the actual reference to the ManyToManyField". But in some rare cases you might need widgets be displayed on both models (some people think it's abnormal in common and some think you shouldn't tune your models for such tasks, but forms instead).

Since I was a newbie I had written:

class User(models.Model):
    #...
    groups = ManyToManyField('Group', related_name='groups')
class Group(models.Model):
    #...
    users = ManyToManyField(User, related_name='users')


And before I looked into database I thought I got expected result... Yes, django has created two different tables for two relations: User_Group and Group_User. And it's pretty logical for models.

Then you can try:


class User(models.Model):
    #...
    groups = ManyToManyField('Group', related_name='groups',
            db_table=u'USERS_TO_GROUPS')
class Group(models.Model):
    #...
    users = ManyToManyField(User, related_name='users',
            db_table=u'USERS_TO_GROUPS')


If you use it with existing db then it will be fine, but if you try syncdb it will try to create two identical tables. So we came to the final snippet:


class ManyToManyField_NoSyncdb(models.ManyToManyField):
    def __init__(self, *args, **kwargs):
        super(ManyToManyField_NoSyncdb, self).__init__(*args, **kwargs)
        self.creates_table = False

class User(models.Model):
    #...
    groups = ManyToManyField('Group',related_name='groups',db_table=u'USERS_TO_GROUPS')
class Group(models.Model):
    #...
    users = ManyToManyField_NoSyncdb(User,related_name='users',db_table=u'USERS_TO_GROUPS')


Here it is: now ManyToManyField widget is displayed on both forms and of cource on admin add/change pages for both models.

In case you use Django Admin you can try to inline intemediary model, but it will be just a kind of a many-to-many relation (no multiselect, but lot of inlines models). In our case it's not a good solution.

Since you work with m-2-m I want to point you to a good snippet for integrating legacy databases: http://www.djangosnippets.org/snippets/962/
In my code I have combined it with my snippet.

11 comments:

Martin said...

Powerfox, thanks for sharing! First hit on google for "django manytomany both models"... Very good example!

Pieter J. Kersten said...

Have you tried 'symmetrical = False' in ManyToManyField? It will create the same result: one table and related_name in the other class...
Saves some code.

Cheers

powerfox said...

Sorry, didn't get notification about comments.
Pieter, docs say ManyToManyField.symmetrical should be used with ManyToManyFields on self only. So if the result is the same it could be a bug (and it can be fixed in next releases).

Philgo20 said...

So what's the good way to do this ? Does the use of symmetrical is ok ? I read the same in the doc but it would seem a little cleaner.

Lobo Mal said...

Dude, loved your snippet! Saved my as big time, here. Using symmetrical=False didn't do the trick here, tough.

Evgeniy Ivanov said...

I'm glad it has helped :)

Federico said...

Great snippet!
Most elegant solution I found for the problem.
If it's OK for you I'll post this in my (spanish) blog.
Obviously, I'll link back here as original post.

Evgeniy Ivanov said...

Federico, sure! I'm glad it helped you :-)

Valkyrie Savage said...

Still useful two years later. :) Thanks!

Gaz Robertson said...

This seems to work for me on Django 1.3 without causing a syncdb problem:

class User(models.Model):
#...
groups = ManyToManyField('Group')
class Group(models.Model):
#...
users = ManyToManyField(User,through=WebPage.categories.through)

Liu Bo said...

Very helpful, but in Django 1.2, it should like this:

class Test1(models.Model):
tests2 = models.ManyToManyField('Test2', blank=True)

class Test2(models.Model):
tests1 = models.ManyToManyField(Test1, through=Test1.tests2.through, blank=True)

I found that at this ticket.