I have a general question regarding the ordering in
the ManyRelatedManager object. For example,
There are two classes in my model: one is
called Book for books and another is Author for authors.
A many-to-many field is defined in class Book
authors = models.ManyToManyField(Author)
In database, the many-to-many relationship
is described in an additional table book_author
with three columns: (id, book_id, author_id)
Now for a Book object b, both b.authors.all() and b.authors.iterator()
are sorted in the order of book_author.author_id.
But what I want is to retrieve the authors of b
in the order of book_author.id because this is
the right order of the authors.
Any solutions? Many thanks in advance!
If you want different Author ordering based on book, I would suggest using a ManyToMany field and specifying the through attribute as described here:
http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships
If your through model defines an ordering column, you could query the through model directly, e.g.
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField('Author', through='BookAuthor')
class Author(models.Model):
name = models.CharField(max_length=100)
class BookAuthor(models.Model):
book = models.ForeignKey(Book)
author = models.ForeignKey(Author)
ordering = models.IntegerField()
You can now query the through model directly if you'd like:
b = Book.objects.get(pk=1)
BookAuthor.objects.filter(book=b).order_by('ordering')
You can also add the Meta ordering to add the order_by whenever querying this model...
class BookAuthor(models.Model):
# ...
ordering = models.IntegerField()
class Meta:
ordering = ('ordering',)
Now you can remove extra .order_by clause, as Django will add this itself.
b = Book.objects.get(pk=1)
BookAuthor.objects.filter(book=b)
Note: Even with a through model and a Meta ordering, Django will not add the .order_by when querying one side or the other, e.g. the following will not have the ORDER BY SQL added:
b = Book.objects.get(pk=1)
b.authors.all() # Not ordered
The "authors" attribute of a Book instance is a manager, so manager method order_by() will work on it:
book = Book.objects.get(id=1)
authors = book.authors.order_by('-last_name')
(assuming you have a "last_name" field on Author)
Related
The django documentation is pretty clear on how to look up fields on single instances but what about reverse relationship QuerySet objects.
Example:
class Author(models.Model)
name = models.Charfield(...)
class Book(models.Model)
title = models.Charfield(...)
author = models.ForeignKey(Author, ...)
I now create a queryset object:
special_books = Book.objects.filter(title__icontains="Space")
Now, I want to find all authors that belong to special_books. I want to do something like this:
special_books.authors.all()
Without doing this:
authors = []
for book in special_books:
authors += book.author
Is there a way to do this easily?
Use the referenced Model name (Book in lowercase) to make your query:
Author.objects.filter(book__title__icontains="Space")
If you have a related_name defined in your foreignKey :
class Book(models.Model)
title = models.Charfield(...)
author = models.ForeignKey(Author, related_name="books")
Your queryset would be :
Author.objects.filter(books__title__icontains="Space")
Quoting Django's documentation :
Related managers support field lookups as well. The API automatically follows relationships as far as you need. Use double underscores to separate relationships. This works as many levels deep as you want.
Simply do the lookup on Author model that spans relationship
Author.objects.filter(book__title__icontains="Space")
Suppose I have following models:
class Author(Model):
name = CharField()
class Publication(Model):
name = CharField()
authors = ManyToManyField(Author)
class Meta:
abstract = True
class Book(Publication):
pass
class Article(Publication):
pass
class Journal(Publication):
pass
How to change code so that I can add through table to authors? If I write authors = ManyToManyField(Author, through='Relationship'), it will not work.
Django thankfully takes care of intermediate tables without any coding. You can even access them using .through on the M2M relationship manager, e.g. one_publication.authors.through.
You only need to specify the through table if you want to manage it yourself, e.g. because you want to add more fields than just the foreign keys of the two related entities. Is that the case here?
If yes, you have to create a Relationship model (consider giving it a more helpful name) that contains foreign keys to Publication and Author.
Update: If you want to add a default order to object lists from many-to-many relationships, an intermediate model would indeed be one way to achieve this:
class Relationship(models.Model):
author = models.ForeignKey(Author)
publication = models.ForeignKey(Publication)
# Any further fields that you need
class Meta:
ordering = ['author__last_name', 'author__first_name']
However, you can just as easily order your m2m relationships on querying them, without any intermediate model or through manager:
book = Book.objects.first()
for author in book.authors.order_by('last_name', 'first_name'):
# Will print in alphabetical order
print(f'Author: {author.first_name} {author.last_name}')
One caveat is that if you use prefetching, you need to specify the ordering in a Prefetch object to avoid executing the query twice, first for prefetching without ordering, than on access with ordering.
# 2 queries plus one for every book
books = Book.objects.prefetch_related('authors')
for book in books:
for author in book.authors.order_by('last_name', 'first_name'):
print(f'Author: {author.first_name} {author.last_name}')
# 2 queries regardless of number of books
books = Book.objects.prefetch_related(Prefetch('authors',
queryset=Author.objects.order_by('last_name', 'first_name')))
for book in books:
for author in book.authors.all():
print(f'Author: {author.first_name} {author.last_name}')
Given the following models:
class Author(models.Model):
name = models.CharField()
class Book(models.Model):
title = models.CharField()
published_year = models.PositiveIntegerField()
authors = models.ManyToManyField(Author)
Let's say I want to get all of the authors who have authored a book published in the year 2008. I can do the following:
Book.objects.filter(published_year=2008).values_list('authors__name').distinct()
That'll get me a list of authors - almost exactly what I want, except that instead of just the names, I want the Author objects. I can achieve that somewhat by doing this:
authors = []
for b in Book.objects.filter(published_year=2008):
for a in b.authors.all():
if a not in authors:
authors.append(a)
But that seems totally unnecessary. Is it possible to get the QuerySet to do that work for me? Thanks!
Just use backward relationship
Author.objects.filter(book__published_year=2008).all()
From Django docs
Reverse m2m queries are supported (i.e., starting at the table that
doesn’t have a ManyToManyField):
I needed to assign one or more categories to a list of submissions, I initially used a table with two foreign keys to accomplish this until I realized Django has a many-to-many field, however following the documentation I haven't been able to duplicate what I did with original table.
My question is : Is there a benefit to using many-to-many field instead of manually creating a relationship table? If better, are there any example on submitting and retrieving many-to-many fields with Django?
From the Django docs on Many-to-Many relationships:
When you're only dealing with simple many-to-many relationships such
as mixing and matching pizzas and toppings, a standard ManyToManyField
is all you need. However, sometimes you may need to associate data
with the relationship between two models.
In short: If you have a simple relationship a Many-To_Many field is better (creates and manages the extra table for you). If you need multiple extra details then create your own model with foreign keys. So it really depends on the situation.
Update :- Examples as requested:
From the docs:
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)
You can see through this example that membership details (date_joined and invite_reason) are kept in addition to the many-to-many relationship.
However on a simplified example from the docs:
class Topping(models.Model):
ingredient = models.CharField(max_length=128)
class Pizza(models.Model):
name = models.CharField(max_length=128)
toppings = models.ManyToManyField(Topping)
There seems no need for any extra data and hence no extra model.
Update 2 :-
An example of how to remove the relationship.
In the first example i gave you have this extra model Membership you just delete the relationship and its details like a normal model.
for membership in Membership.objects.filter(person__pk=1)
membership.delete()
Viola! easy as pie.
For the second example you need to use .remove() (or .clear() to remove all):
apple = Toppings.objects.get(pk=4)
super_pizza = Pizza.objects.get(pk=12)
super_pizza.toppings.remove(apple)
super_pizza.save()
And that one is done too!
I have the following two models
class Author(Model):
name = CharField()
class Publication(Model):
title = CharField()
And I use an intermediary table to keep track of the list of authors. The ordering of authors matter; and that's why I don't use Django's ManyToManyField.
class PubAuthor(Model):
author = models.ForeignKey(Author)
pubentry = models.ForeignKey(Publication)
position = models.IntegerField(max_length=3)
The problem is, given a publication, what's the most efficient way to get all authors for the publication?
I can use pubentry.pubauthor_set.select_related().order_by('position'), but then it this will generate one query each time I access the author's name.
I've found out the answer.
In publications:
def authors(self):
return Author.objects.all().filter(
pubauthor__pubentry__id=self.id).order_by('pubauthor__position')