views:

146

answers:

3

I have two django model classes:

class A(models.Model):
     name = models.CharField(max_length = 128)    #irrelevant

class B(models.Model):
     a = models.ManyToManyField(A)
     name = models.CharField(max_length = 128)    #irrelevant

What I want to do is the following:

a1 = A()
a2 = A()
b = B()

b.a.add(a1)
b.a.add(a1)    #I want to have a1 twice
b.a.add(a2)

assert len(b.a.all()) == 3 #this fails; the length of all() is 2

I am guessing that add() uses a set semantic, but how can I circumvent that? I tried looking into custom managers, but I am not sure if this the right way (seems complicated)...

Thanks in advance!

+1  A: 

Django uses a relational DB for its underlying storage, and that intrinsically DOES have "set semantics": no way to circumvent THAT. So if you want a "multi-set" of anything, you have to represent it with a numeric field that counts how many times each item "occurs". ManyToManyField doesn't do that -- so, instead, you'll need a separate Model subclass which explicitly indicates the A and the B it's relating, AND has an integer property to "count how many times".

Alex Martelli
I think that's not entirely correct. The relation from A to B is realized via a relation table comprised of three columns: id, a_id and b_id. id is the primary key of that table, so it would be OK to have three rows in that table where two have the same entry for a_id and b_id respectively. And this is what I want, but I can't figure out how to tell Django that...
qollin
@qolin, sos-skil's answer offers you a way to make such a table (not via manytomany of course) -- my idea is just to add a count field to that table rather than relying on duplicates (it's just a more compact way to implement multisets aka bags).
Alex Martelli
@qollin: I'm not sure you can rely on there always being an id in the relationship table - that may be an implementation detail. And you can build a many-to-many using a "through" table which can have additional columns, such as the count field that Alex suggested.
Vinay Sajip
A: 

One way to do it:

class A(models.Model):
    ...

class B(models.Model):
    ...

class C(models.Model):
    a = models.ForeignKey(A)
    b = models.ForeignKey(B)

Then:

>>> a1 = A()
>>> a2 = A()
>>> b = B()
>>> c1 = C(a=a1, b=b)
>>> c2 = C(a=a2, b=b)
>>> c3 = C(a=a1, b=b)

So, we simply have:

>>> assert C.objects.filter(b=b).count == 3
>>> for c in C.objects.filter(b=b):
...     # do something with c.a
skyl
I know, not exactly what you are looking for but a way to do something similar :/
skyl
+1  A: 

I think what you want is to use an intermediary model to form the M2M relationship using the through keyword argument in the ManyToManyField. Sort of like the first answer above, but more "Django-y".

class A(models.Model):
    name = models.CharField(max_length=200)

class B(models.Model):
    a = models.ManyToManyField(A, through='C')
    ...

class C(models.Model):
    a = models.ForeignKey(A)
    b = models.ForeignKey(B)

When using the through keyword, the usual M2M manipulation methods are no longer available (this means add, create, remove, or assignment with = operator). Instead you must create the intermediary model itself, like so:

 >>> C.objects.create(a=a1, b=b)

However, you will still be able to use the usual querying operations on the model containing the ManyToManyField. In other words the following will still work:

 >>> b.a.filter(a=a1)

But maybe a better example is something like this:

>>> B.objects.filter(a__name='Test')

As long as the FK fields on the intermediary model are not designated as unique you will be able to create multiple instances with the same FKs. You can also attach additional information about the relationship by adding any other fields you like to C.

Intermediary models are documented here.

jdl2003