17

There are several question about backwards relations here, but I'm either too dumb to understand them or think they don't fit my case.

I have model

class MyModel(models.Model)
    stuff = models.ManyToManyField('self', related_name = 'combined+')

I have created form where i combine the information stored. And it stores object relations in database like that:

Table:

id:from_stuff_id:to_stuff_id
1:original_object_id:first_related_object
2:original_object_id:second_related_object
3:first_related_object:original_object_id
4:second_related_object:original_object_id

So when i display object first_related_object and check for relations with

myobject.stuff.all()

Then i get the "original_object". But i do not need it. I wish it would show no backwards relation like that.

Edit1

So i suck at explaining myself.

Perhaps this code will better illustrate what i want.

myobjectone = MyModel.objects.get(pk = 1)
myobjecttwo = MyModel.objects.get(pk = 2)
myobjectthree = MyModel.objects.get(pk = 3)
myobjectone.stuff.add(myobjecttwo)
myobjectone.stuff.add(myobjectthree)
myobjectone.stuff.all()
[myobjecttwo, myobjectthree] <-- this i want
myobjecttwo.stuff.all()
[myobjectone]<-- this i do not want
myobjectthree.stuff.all()
[myobjectone]<-- this i do not want

Now only question is - if i should even use stuff.all() if i dont want the results they yield and should write my own manager/methods to get list of objects which excludes backward relations.

/edit1

edit2 In response to Henry Florence:

Okay - i did test it with empty base and it looks like symmetrical = False does have database level differences. I think.

I created empty table with symmetrical = False, then creating adding relations did not spawn backwards relations. If i created empty table without symmetrical = False. Then they were. Setting symmetrical = False makes no difference AFTER tables have been created. So i guess the differences are on database level.

/edit2 So what am i supposed to do here?

Write my own manager or something?

Alan

Odif Yltsaeb
  • 5,575
  • 12
  • 49
  • 80
  • Do you mean excluding the current object from the results, like `myobject.stuff.exclude(id=self.id)`? – Timmy O'Mahony Nov 07 '13 at 14:02
  • Nope. i mean excluding 3rd and 4th row in table from results when queried with myobject.stuff.all(), myobject beeing the first_related_object. I dont want the original_object to show from other objects .all() queries. – Odif Yltsaeb Nov 07 '13 at 14:09
  • I'm not sure I understand the question, but I think you might be looking for the [`symmetrical`](https://docs.djangoproject.com/en/1.6/ref/models/fields/#django.db.models.ManyToManyField.symmetrical) option. Try setting `symmetrical=False`. – Alasdair Nov 07 '13 at 14:54
  • Yeah i know - i suck at explaining. Symmetrical might do some of the work, but i'm not seeing desired results. – Odif Yltsaeb Nov 07 '13 at 15:24

1 Answers1

30

My understanding of the question is that the ManyToMany relationship should be one way, in that if the following is true:

             --------------           ----------------
             | mymodelone |---------->|  mymodeltwo  |
             --------------     |     ----------------
                                |
                                |     ----------------
                                ----->| mymodelthree |
                                      ----------------

Then there should not be an implicit relationship in the other direction:

             --------------           ----------------
             | mymodelone |<-----/----|  mymodeltwo  |
             --------------           ----------------

             --------------           ----------------
             | mymodelone |<-----/----| mymodelthree |
             --------------           ----------------

ManyToMany fields have a symmetrical property which by default is True, see: here.

To create a new app to demonstrate the non symmetric ManyToMany field:

Create a new app:

$ python ./manage.py startapp stuff

Add stuff app in settings.py:

...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'stuff'
)
....

edit `stuff/models.py:

from django.db import models

class MyModel(models.Model):
    stuff = models.ManyToManyField('self', related_name = 'combined+', symmetrical=False, blank = True, null = True, verbose_name = "description")

    def __unicode__(self):
        return "MyModel%i" % self.id

Sync the db:

$ python ./manage.py syncdb
Creating tables ...
Creating table stuff_mymodel_stuff
Creating table stuff_mymodel
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

and then test in the django shell:

$ python ./manage.py shell
>>> from stuff.models import MyModel
>>> MyModel().save()
>>> MyModel().save()
>>> MyModel().save()
>>> MyModel.objects.all()
[<MyModel: MyModel1>, <MyModel: MyModel2>, <MyModel: MyModel3>]
>>> m1 = MyModel.objects.get(pk=1)
>>> m2 = MyModel.objects.get(pk=2)
>>> m3 = MyModel.objects.get(pk=3)
>>> m1.stuff.all()
[]
>>> m1.stuff.add(m2)
>>> m1.stuff.add(m3)
>>> m1.stuff.all()
[<MyModel: MyModel2>, <MyModel: MyModel3>]
>>> m2.stuff.all()
[]
>>> m3.stuff.all()
[]
>>> 

Edit - ManyToMany relationship on existing models

The symmetry of the ManyToManyField is created when the models are written to the database, rather than when they are read. If we alter the model to:

from django.db import models

class MyModel(models.Model):
    stuff = models.ManyToManyField('self', related_name = 'combined+')

def __unicode__(self):
    return "MyModel%i" % self.id

Create new MyModel instances:

>>> MyModel().save()
>>> MyModel().save()
>>> MyModel.objects.all()
[<MyModel: MyModel1>, <MyModel: MyModel2>, <MyModel: MyModel3>, <MyModel: MyModel4>, <MyModel: MyModel5>]
>>> m4 = MyModel.objects.get(pk=4)
>>> m5 = MyModel.objects.get(pk=5)
>>> m4.stuff.add(m5)
>>> m4.stuff.all()
[<MyModel: MyModel5>]
>>> m5.stuff.all()
[<MyModel: MyModel4>]

As expected the stuff ManyToManyField is creating symmetrical relations. If we then set the ManyToManyField to symmetrical = False:

>>> from stuff.models import MyModel
>>> MyModel().save()
>>> MyModel().save()
>>> MyModel.objects.all()
[<MyModel: MyModel1>, <MyModel: MyModel2>, <MyModel: MyModel3>, <MyModel: MyModel4>, <MyModel: MyModel5>, <MyModel: MyModel6>, <MyModel: MyModel7>]
>>> m6 = MyModel.objects.get(pk=6)
>>> m7 = MyModel.objects.get(pk=7)
>>> m6.stuff.all()
[]
>>> m7.stuff.all()
[]
>>> m6.stuff.add(m7)
>>> m6.stuff.all()
[<MyModel: MyModel7>]
>>> m7.stuff.all()
[]
>>> m5 = MyModel.objects.get(pk=5)
>>> m4 = MyModel.objects.get(pk=4)
>>> m4.stuff.all()
[<MyModel: MyModel5>]
>>> m5.stuff.all()
[<MyModel: MyModel4>]

It can be seen the new ManyToMany relation between m6 and m7 is not symmetrical, however the existing one, between m4 and m5 is still symmetrical as the model stated when those objects were created.

Edit - additional database constraints with a symmetrical foreign key

Apologies to the reader to the length of this answer, we seem to be exploring this problem at some depth.

In sql a many to many relation is modeled by creating a table that holds all the information unique to the relation - usually just the primary key value of the two tables.

So for our MyModel, django creates two tables:

             -----------------           -----------------------
             | stuff_mymodel |---------->| stuff_mymodel_stuff |
             -----------------           -----------------------
                      ^                              |
                      |                              |
                      --------------------------------

The links shown in the diagram are represented by primary key or id values within the schema:

mysql> describe stuff_mymodel;
+-------+---------+------+-----+---------+----------------+
| Field | Type    | Null | Key | Default | Extra          |
+-------+---------+------+-----+---------+----------------+
| id    | int(11) | NO   | PRI | NULL    | auto_increment |
+-------+---------+------+-----+---------+----------------+
1 row in set (0.00 sec)

mysql> describe stuff_mymodel_stuff;
+-----------------+---------+------+-----+---------+----------------+
| Field           | Type    | Null | Key | Default | Extra          |
+-----------------+---------+------+-----+---------+----------------+
| id              | int(11) | NO   | PRI | NULL    | auto_increment |
| from_mymodel_id | int(11) | NO   | MUL | NULL    |                |
| to_mymodel_id   | int(11) | NO   | MUL | NULL    |                |
+-----------------+---------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

And shown as the output from the Django manage.py script:

$ python ./manage.py sql stuff
    BEGIN;
CREATE TABLE `stuff_mymodel_stuff` (
    `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `from_mymodel_id` integer NOT NULL,
    `to_mymodel_id` integer NOT NULL,
    UNIQUE (`from_mymodel_id`, `to_mymodel_id`)
)
;
CREATE TABLE `stuff_mymodel` (
    `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY
)
;
ALTER TABLE `stuff_mymodel_stuff` ADD CONSTRAINT `from_mymodel_id_refs_id_7fa00238` FOREIGN KEY (`from_mymodel_id`) REFERENCES `stuff_mymodel` (`id`);
ALTER TABLE `stuff_mymodel_stuff` ADD CONSTRAINT `to_mymodel_id_refs_id_7fa00238` FOREIGN KEY (`to_mymodel_id`) REFERENCES `stuff_mymodel` (`id`);
COMMIT;

This sql is the same irrespective if the django ManyToManyField is symmetrical or not. The only difference is the number of rows created in the stuff_mymodel_stuff table:

mysql> select * from stuff_mymodel_stuff;
+----+-----------------+---------------+
| id | from_mymodel_id | to_mymodel_id |
+----+-----------------+---------------+
|  1 |               1 |             2 |
|  2 |               1 |             3 |
|  3 |               4 |             5 |
|  4 |               5 |             4 |
|  5 |               6 |             7 |
+----+-----------------+---------------+
5 rows in set (0.00 sec)

The link m4 -> m5 being symmetrical and the others not. Digging around in the Django source we can find the code responsible for creating the 'mirror' entry in the sql, if symmetrical is True:

    # If the ManyToMany relation has an intermediary model,
    # the add and remove methods do not exist.
    if rel.through._meta.auto_created:
        def add(self, *objs):
            self._add_items(self.source_field_name, self.target_field_name, *objs)

            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
            if self.symmetrical:
                self._add_items(self.target_field_name, self.source_field_name, *objs)
        add.alters_data = True

This is currently line 605 on github: https://github.com/django/django/blob/master/django/db/models/fields/related.py

Hope this answers all your queries.

Henry Florence
  • 2,848
  • 19
  • 16
  • Yes this is what i am looking for, but this is not what i see, when i do the same thing you described. And i have this field declared like this : stuff = models.ManyToManyField('self', related_name = 'combined+', symmetrical=False, blank = True, null = True, verbose_name = _(u"description")) – Odif Yltsaeb Nov 11 '13 at 07:47
  • why is the string for verbose_name in your comment written like that? It throws an error for me. I had to create and persist new MyModels to get the code above to work, are there stale versions in your db? – Henry Florence Nov 11 '13 at 10:08
  • verbose_name is just from the real model not the simple example i used to illustrate problem here. And yes - i have not touched database since the model changes, because i read somewhere that these related_name and symmetrical changes do not need to touch database anyway. Was i wrong? – Odif Yltsaeb Nov 11 '13 at 10:47
  • and in fact it seems they don't hit the database cause: python manage.py schemamigration myapp --auto Nothing seems to have changed. – Odif Yltsaeb Nov 11 '13 at 10:49
  • ah ok, I've edited my post to show how to create the model in a new app and demonstrate creating a non symmetric relation, this definitely works in Django 1.4-1.6, I copied the shell output from the shell. Alternatively, can you post / link to more of your code, I would be happy to debug it. – Henry Florence Nov 11 '13 at 11:12
  • In all important matters - my code looks EXACTLY like yours. I also use django 1.4. Only difference is that i use django-south to migrate the stuff. From the tables that syncdb creates for your case i guess that i got exactly the same tables. I even desribed the columns and values which you get in stuff_mymodel_stuff in my original post (id:from_stuff_id:to_stuff_id). The fact is though - when i run the commands you do, in shell, then they work as i describe, not as you describe. Like i said - the symmetrical or related_name make no difference in database. – Odif Yltsaeb Nov 11 '13 at 11:17
  • Can you try running the commands in the post to create a new app called stuff to simply test the code on your machine? Otherwise, post more of your app and I will be happy to help you debug it. – Henry Florence Nov 11 '13 at 11:25
  • @OdifYltsaeb I agree - an issue with South or the database structure would seem very unlikely. Are you creating new objects to test the `ManyToMany` relation? See the edit in my post above. – Henry Florence Nov 11 '13 at 13:11
  • I'll try it later today. Other stuff is keeping me busy right now. – Odif Yltsaeb Nov 11 '13 at 13:22
  • Allright. I tested it and it seems you are right. If table is created with symmetrical = False set, then the backwards relations are not created. Setting/removing symmetrical = False after tables have been created makes no difference. See Edit2. – Odif Yltsaeb Nov 11 '13 at 17:26
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/40971/discussion-between-henry-florence-and-odif-yltsaeb) – Henry Florence Nov 11 '13 at 17:29
  • allright. I checked out what kind of differences are there on database level and there are several additional constraints added to tables when manytomany field is defined with symmetrical = False. I'm sure if you open your database and see how tables were created (export as sql), then you get the information too. Just add it to your answer and ill accept it. – Odif Yltsaeb Nov 12 '13 at 09:17