Django, update the object after a prefetch_related - python

I have the following models:
class Publisher(models.Model):
name = models.CharField(max_length=30)
class Book(models.Model):
title = models.CharField(max_length=100)
publisher = models.ForeignKey(Publisher)
In my views.py, When I want to show the publisher page, I also want to show their books, so I usually do something like this:
publisher = Publisher.objects.prefetch_related('book_set').filter(pk=id).first()
Then, after some processing I also do some work with the books
for book in publisher.book_set.all():
foo()
This works great, but I have one problem. If there is a book added between the query and the for loop, the publisher.book_set.all() won't have the newly added books because it was prefetched.
Is there a way to update the publisher object?

You can delete the entire prefetch cache on the instance:
if hasattr(publisher, '_prefetched_objects_cache'):
del publisher._prefetched_objects_cache
If you only want to delete a particular prefetched relation:
if hasattr(publisher, '_prefetched_objects_cache'):
publisher._prefetched_objects_cache.pop('book_set', None)

There's a nicer way to clear prefetched attributes, using only public Django APIs, which is the refresh_from_db() method:
# Reloads all fields from the object as well as clearing prefetched attributes
publisher.refresh_from_db()
# Clears one prefetched attribute, will be re-fetched on next access
publisher.refresh_from_db(fields=['book_set'])
for book in publisher.book_set.all():

Also there is possibility to drop all prefetch_related from Django doc:
To clear any prefetch_related behavior, pass None as a parameter::
non_prefetched = qs.prefetch_related(None)

Related

Django many to many relation, include all IDs in queryset in both directions

I have 2 models connected via M2M relation
class Paper(models.Model):
title = models.CharField(max_length=70)
authors = models.ManyToManyField(B, related_name='papers')
class Author():
name = models.CharField(max_length=70)
Is there a way to include authors as all related authors' IDs (and maybe name somehow)?
Is there a way to include papers IDs as reverse relation (and maybe title as well)?
Author.objects.all().annotate(related_papers=F('papers'))
this only adds id of one paper, first one it finds I think.
Furthermore, changing related_papers to papers gives an error:
ValueError: The annotation ‘papers’ conflicts with a field on the
model.
From what I understand in your comments, you're using DRF. I will give you 2 answers.
1) If you're talking about model serializer, you can use PrimaryKeyRelatedField :
class AuthorSerializer(serializers.ModelSerializer):
papers=serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Author
fields = ['name', 'papers']
class PaperSerializer(serializers.ModelSerializer):
class Meta:
model = Paper
fields = '__all__'
This will return the IDs for the other side of the relationship whether you're on Paper or Author side. That will return the primary keys, not a representation of the object itself.
2) Now you're also talking about performance (e.g. database hit at each iteration).
Django (not DRF-specific) has a queryset method to handle preloading related objects. It's called prefetch_related.
For example, if you know you're going to need the relation object attributes and want to avoid re-querying the database, do as follow:
Author.objects.all().prefetch_related('papers')
# papers will be already loaded, thus won't need another database hit if you iterate over them.
Actually, it has already been implemented for you. You should include a Many-to-Many relationship to author in your Paper model like this:
class Paper(models.Model):
title = models.CharField(max_length=70)
authors = models.ManyToManyField(Author, related_name='papers')
That gives you the opportunity to add Author objects to a related set using
p.authors.add(u), assuming that p is the object of Paper model, and a is an object of Author model.
You can access all related authors of a Paper instance using p.authors.all().
You can access all related papers of an Author instance using u.papers.all().
This will return an instance of QuerySet that you can operate on.
See this documentation page to learn more.

Recursive delete foreign keys for Django object

Suppose I have an object called Person that has a foreign key that links to CLothes which links to
class Person(models.Model):
clothes = models.ForeignKey('Clothes', on_delete=models.PROTECT)
jokes = models.ManyToManyField(to='Jokes')
class Clothes(models.Model):
fabric = models.ForeignKey('Material', on_delete=models.PROTECT)
class Material(models.Model):
plant = models.ForeignKey('Plant', on_delete=models.PROTECT)
And if I wanted to delete person, I would have to delete Clothes, Jokes, Materials attached to it. Is there a way to recursively detect all the foreign keys so that I can delete them?
The django.db.models.deletion.Collector is suited for this task. It is what Django uses under the hood to cascade deletions.
You can use it this way:
from django.db.models.deletion import Collector
collector = Collector(using='default') # You may specify another database
collector.collect([some_instance])
for model, instance in collector.instances_with_model():
# Our instance has already been deleted, trying again would result in an error
if instance == some_instance:
continue
instance.delete()
For more information about the Collector class, you can refer to this question:
How to show related items using DeleteView in Django?
As mentioned in the comments, using on_delete=models.CASCADE would be the best solution but if you do not have control over that, this should work.

How to make superclass querysets return subclass objects? Or downcast the superclass queryset to a subclass list?

I'm working in a django project which I need to list two different models in the same view ordered by date. In order to achieve that I used inheritance to be able to get them all into a generic queryset. My models are:
class Publication(models.model):
title = models.CharField(max_lengh = 200)
pub_date = models.DateTimeField(default = datetime.now)
headline = models.TextField()
class Meta:
abstract = True
#abc.abstractmethod
def say_hello(self):
return
class New(Publication):
author = models.ForeignKey(Author)
source = models.CharField(max_length = 200)
categories = models.ManyToManyField(Category)
url = '/news/'
def say_hello(self):
return "Hello New!!!"
class Opinion(Publication):
writer = models.ForeignKey(Writer)
style = .models.CharField(max_length=3, choices=(('txt', 'Text'), ('glr', 'Galery')))
url = '/opinions/'
def say_hello(self):
return "Hello Opinion!!!"
I'm trying to call the subclass method while iterating through the Publication QuerySet like this:
publications = Publications.objects.all().order_by('-pub_date')
for pub in publications:
pub.say_hello()
url = pub.url
The problem is that my QuerySet is returning Publication objects, so I can't access child attributes and methods, obviously cus I'm dealing with Publication objects. Shouldn't The fact that I've set Publication as an abstract class, avoid the possibility of dealing with Publication objects?. Shouldn't they be prevented from being instantiated? Is there any option for perform perform a QuerySet in Publication class and return a list with child objects?
If no. How would you guys go around this situation? I could really use some tips.
Sounds like it might be appropriate to use multi-table inheritance and django polymorphic:
Multi-table inheritance: https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance
Django polymorphic: http://django-polymorphic.readthedocs.org/en/latest/
Multi-table inheritance in django allows you to have a base model/table which has your base fields. Your subclasses then define the extended fields which are put in their own tables. When you fetch records with querysets from any of the subclasses, you'll get information for each record from both the base model/table and the subclass model/table.
In order to fetch records using the base model's queryset, and get an instance of the appropriate subclass for each result, one option is django polymorphic. I've used it before and it works pretty well. It definitely has its limitations but I'd give it a shot.
Each Publication instance should have either a 'new' attribute or a 'opinion' attribute pointing to one of the two subclasses respectively. Be aware that each instance has only one of this attributes so maybe it's better to try...except access to them.
Well, I will put the code for my solution here which I achieved thanks to #David answer.
As suggested for David, I used django-polymorphic, which is great and simple. But the fact that I already had a populated database, made things a bit complicated. Nothing hard to fix.
First thing I did was to migrate the database with south in order to add the new field (polymorphic_ctype) to my parent model (no field is added to the subclasses).
Then, I used the following code in django shell mode on terminal. (python manage.py shell)
from jornal.models import Publication, New, Opinion
from django.contrib.contenttypes.models import ContentType
ctype_opinion = ContentType.objects.get(model = 'opinion', app_label = 'jornal')
ctype_new = ContentType.objects.get(model = 'new', app_label = 'jornal')
opinions = Opinion.objects.non_polymorphic().all()
news = New.objects.non_polymorphic().all()
for new in news:
new.polymorphic_ctype = ctype_new
new.save()
for opinion in opinions:
opinion.polymorphic_ctype = ctype_opinion
opinion.save()

Django: Create Submodel in Model

Suppose I have two models, Book and Page, such there is a one-to-many relationship between Page to Book. I would like each Book model, to have a function which creates a bunch of Pages for the respective Book model, but I'm not sure how to associate each Page's foreign key back to the Book instance it was created in.
I currently have an associate function, which finds the correct book amongst all the possible books, and I'm sure there's a better way, but I can't seem to find it. Is it possible to set the foreignkey to self?
class Page(models.Model):
# default related_name would be `page_set` but `pages`
# seems a bit more intuitive, your choice.
book = models.ForeignKey("yourapp.Book", related_name="pages")
class Book(models.Model):
# ...
# you can use self as the instance and magically you have
# this `pages` attr which is a related manager, like `Page.objects`
# but limited to the particular Book's pages.
def special_page_create(self, *args, **kwargs):
self.pages.create(
# ...
)
# ...
then, you have a book instance, you have a related manager,
book = Book.objects.get(...
book.pages.create(...
page, created = book.pages.get_or_create(...
pages_gte_42 = book.pages.filter(num__gte=42)
# etc
let me know what else you need to know. The reverse relationship is semi-magical; you don't declare anything on the Book class but you get the related manager on the Book instances.
would try something like this:
Class Page(models.Model):
#Your fields, like a name or whatever
name = models.CharField(max_length=60)
Book= models.ForeignKey("Book")
Class Book(models.Model):
#Your fields again
def create_page(self):
"Adds a page to the book"
#Do what you need to do
page = Page(name="TitlePage", Book=self)
page.save()
Or something like that... I think the order of the classes is important too.
There is a one-to-many relationship between Book and Page. This means that one Book has several Pages and each Page only one Book.
In a database (and therefore in an ORM mapper) you would model this by creating a foreign key in Page. This foreign key would point to the Book instance that contains these Pages.
If you want to find the Pages for a Book, you'd query the Pages table for all Pages with the same foreign key as the primary key of the Book.

Django: saving a ModelForm with custom many-to-many models

I have Publications and Authors. Since the ordering of Authors matters (the professor doesn't want to be listed after the intern that contributed some trivial data), I defined a custom many-to-many model:
class Authorship(models.Model):
author = models.ForeignKey("Author")
publication = models.ForeignKey("Publication")
ordering = models.IntegerField(default=0)
class Author(models.Model):
name = models.CharField(max_length=100)
class Publication(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, through=Authorship)
I've got aModelForm for publications and use it in a view. Problem is, when I call form.save(), the authors are obviously added with the default ordering of 0. I've written a OrderedModelMultipleChoiceField with a clean method that returns the objects to be saved in the correct order, but I didn't find the hook where the m2m data is actually saved, so that I could add/edit/remove the Authorship instances myself.
Any ideas?
If you are using a custom M2M table using the through parameter, I believe you must do the saves manually in order to save the additional fields. So in your view you would add:
...
publication = form.save()
#assuming that these records are in order! They may not be
order_idx = 0
for author in request.POST.getlist('authors'):
authorship = Authorship(author=author, publication=publication, ordering=order_idx)
authorship.save()
order_idx += 1
You may also be able to place this in your ModelForm's save function.
I'm not sure if there's a hook for this, but you could save it manually with something like:
form = PublicationForm(...)
pub = form.save(commit=False)
pub.save()
form.save_m2m()
So you can handle any custom actions in between as required. See the examples in the docs for the save method.

Categories

Resources