I show comments in the topic detail view. Exactly two queries happens
in this view: one to get topic and one to get comments list. I
understand that select_related technique can't be used here since
there are two different querysets created in the view. Is there any
way to decrease number of queries to one?
Application code follows below.
app/models.py
class Topic(models.Model):
headline = models.CharField(max_length=400)
description = models.TextField()
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, related_name='topics')
class Comment(models.Model):
headline = models.CharField(blank=True, max_length=400)
description = models.TextField()
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
author = models.ForeignKey(User, related_name='comments')
topic = models.ForeignKey(Topic, related_name='comments')
app/views.py
class TopicDetail(DetailView):
queryset = Topic.objects.select_related('author').all()
context_object_name = 'topic'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
topic = self.object
context['comments'] = topic.comments.select_related('author').all()
return context
app/templates/app/topic_detail.html
{{ topic.headline }}
{{ topic.description }}
{{ topic.created }}
{{ topic.modified }}
{{ topic.author }}
{% for comment in comments %}
{{ comment.headline }}
{{ comment.description }}
{{ comment.author }}
{% endfor %}
Have a look at the prefetch_related method for reversed foreignKey lookups. This won't actually reduce the number of SQL queries, but django will merge in python related comments and Topics.
In the docs : https://docs.djangoproject.com/en/1.8/ref/models/querysets/#prefetch-related
Also, you should make sure to give different related_name to your fields, otherwise you'll have issues calling them :
author = models.ForeignKey(User, related_name='user_comments')
topic = models.ForeignKey(Topic, related_name='topic_comments')
This way you can write a single line query (but 2 SQL hits) :
topics = Topic.objects.prefetch_related('topic_comments').select_related('author').all()
Then for example :
{% for topic in topics %}
{% for comment in topic.topic_comments.all %}
{{ ... }}
{% endfor %}
{% endfor %}
Even in pure SQL such "single" query would be hard.
Basically You would need to repeat all the topic data in each comment! Like in this pseudo code:
Select comment.*, topic.*
From comments
Right Join topic
That is massive data processing/transfer overhead. Whole operation should take more time, resources then separate queries.
If You really need it, then just write custom function in comment topic model, with pure SQL.
Related
I am trying to consolidate all the comments, on various products, from the logged in user in an "Account" page.
My initial plan was to request all comments from the user id.
Because I created a Profile model, I thought the right way to approach this was to link it to the profile id, and not directly to the use id.
Obviously, it's not working.
Am I close to it? or should I think of it completely differently? (new to programming, as you can see on the code)
Starting with my models.py
class ReviewRating(models.Model):
user = models.ForeignKey(User,blank=True, on_delete=models.CASCADE)
product=models.ForeignKey(Product,related_name="comments", on_delete=models.CASCADE)
review=models.TextField(max_length=250)
def __str__(self):
return '%s - %s - %s'%(self.user, self.product, self.date_added)
class Profile(models.Model):
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
general_reviews = models.ForeignKey(ReviewRating,null=True, on_delete=models.CASCADE)
def str(self):
return str(self.user)
Views.py
def account(response, profile_id):
generalreviews_list = Profile.general_reviews.objects.all(pk=profile_id)
return render(response,"main/account.html", {'generalreviews_list':generalreviews_list})
URLS
path("account/<profile_id>/", views.account, name="account"),
Method - 1 (easiest and logical method)
profile.html:
{% for comment in request.user.reviewrating_set.all %}
{{ comment.review }}
{% endfor %}
Method - 2 (by setting a related_name what Django already defines one called foo_set )
You need to add a related_name attribute to the user field under ReviewRating
class ReviewRating(models.Model):
user = models.ForeignKey(User,blank=True, on_delete=models.CASCADE, related_name="comments")
That's It! You can use it on the profile HTML like this:
profile.html:
{% for comment in request.user.comments %}
{{ comment.review }}
{% endfor %}
The best way is to link it with the main user model in your case it will be best to link it with user_id. After that, you can use the following query to access all the comments made by the currently logged-in user.
Views.py
current_user = request.user
queryset_obj = Model.objects.filter(foreign_key_field_name=current_user)
context = {
'comments' = quesyset_obj,
}
Templates.py
{% for comment in comments %}
{{comment.review}}
{% endfor %}
I've been banging my head against this for an whole workday now and tried various suggestions from stackoverflow and other google results, and consulted the django documentation but all to no avail.
In my django project I have the two models listed below. The related values in the models are the rowid which is unique to each "Sag". My goal is that when I query using "Sag.objects.all()" that it would also return the group attached to that specific rowid.
Ideally it should be a "OneToOneField" and not a "ForeignKey" since the rowid should only exists once in both tables but my latest try had me change it to a "ForeignKey". I have, however, been unable to get the related field with the different solutions I've tried so far
class Sag(models.Model):
id = models.IntegerField(blank=True, null=False, primary_key=True)
rowid = models.IntegerField(db_column='RowId', blank=True, null=True)
posts = models.TextField(db_column='POSTS', blank=True, null=True)
date = models.TextField(db_column='Date', blank=True, null=True)
art = models.IntegerField(db_column='Art', blank=True, null=True)
class Grouping(models.Model):
rowid = models.ForeignKey(Sag, on_delete=models.CASCADE, db_column='RowId')
group = models.IntegerField(db_column='Group', blank=True, null=True)
Any ideas/ressources as to how i would solve this problem?
With a ForeignKey as you have it here, you can pass a related_name. If you don't pass one, Django assigns one by default with the model name and then _set, here it would be sag.grouping_set.all().
sag = Sag.objects.get(id=1)
groupings = sags.grouping_set.all()
Inside a template, if you have a list of Sags:
{% for sag in sags %}
{% for grouping in sag.grouping_set.all %}
{{ grouping.group }}
{% endfor %}
{% endfor %}
To grab the values only:
sag_groups = Sag.objects.all().values('posts', 'data', 'art', 'grouping_set__group')
With a OneToOneField you could do something similar:
sag = Sag.objects.get(id=1)
grouping = sag.grouping
grouping_group = grouping.group
Inside a template:
{% for sag in sags %}
{{ sag.posts }}
{{ sag.grouping.group }}
{{ sag.grouping.rowid}}
{% endfor %}
For the values only:
sag_groups = Sag.objects.all().values('posts', 'data', 'art', 'grouping__group')
I am trying to build a simple web page that queries three tables. There is a Company table that has a one-to-many relationship with a Position table, as well as a one-to-many relationship with a Project table.
The goal is to have the page display a given company once, along with all positions and and projects associated with said company. Then, move on to display the next company, any positions held there and projects completed.
Below is the closest I've come to getting this right. But, the obvious problem is that if there is more than one project associated with a given company, you'll see that company listed more than once.
I'm new to Django, so in the interest of learning, I wanted to beat my own head sufficiently hard before asking for help; but I could really use some fresh ideas at this point.
Also: I can see how a nested for loop might work here, but I'm just not clear on how the mechanics of that would work with the query, and then within the template.
Models:
from django.db import models
class Company(models.Model):
company_id = models.AutoField(primary_key=True)
company_name = models.CharField(max_length=20)
company_logo = models.ImageField(upload_to='images/')
def __str__(self):
return self.company_name
class Position(models.Model):
position_id = models.AutoField(primary_key=True)
position_title = models.CharField(max_length=55)
company_id = models.ForeignKey('professional.Company',
on_delete=models.CASCADE,
blank=True,
null=True)
begin_date = models.DateField()
end_date = models.DateField()
def __str__(self):
return self.position_title
class Project(models.Model):
project_id = models.AutoField(primary_key=True)
project_name = models.CharField(max_length=55)
company_id = models.ForeignKey('professional.Company',
on_delete=models.CASCADE,
blank=True,
null=True)
project_description = models.CharField(max_length=500)
project_image = models.ImageField(upload_to='images/')
def __str__(self):
return self.project_name
View:
from django.views.generic import TemplateView, ListView
from professional.models import Company
class ProfessionalHome(TemplateView):
template_name = 'professional/professional_home.html'
class TechnologyListView(ListView):
template_name = 'professional/__technology.html'
context_object_name = 'technology_list'
def get_queryset(self):
return Company.objects.values('company_name','position__position_title', 'project__project_name')
HTML and template:
{% for job in technology_list %}
<h1>{{job.company_name}}</h1>
<h1>Position: {{job.position__position_title}}</h1>
<h1>project: {{job.project__project_name}}</h1>
{% endfor %}
Instead of values in get_queryset method, you can return the actual queryset and then iterate over it to build your view.
def get_queryset(self):
return Company.objects.all()
Then in your template:
{% for job in technology_list %}
<h1>{{job.company_name}}</h1>
{% for position in job.position_set.all() %}
<h1>Position: {{position.position_title}}</h1>
{% endfor %}
{% for project in job.position_set.all() %}
<h1>project: {{project.project_name}}</h1>
{% endfor %}
{% endfor %}
If you want to iterate over companies, then you should use the Company model as the basis for your view, not Technology. Also, you should avoid values and values_list unless you know you have a good reason, which you don't here. You can use prefetch_related() to reduce the number of reverse queries. So:
class TechnologyListView(ListView):
def get_queryset(self):
return Company.objects.all.prefetch_related('project','position')
...
{% for company in company_list %}
<h1>{{company.company_name}}</h1>
{% for position in company.position_set.all %}
<h1>Position: {{ position.position_title }}</h1>
{% endfor %}
{% for project in company.project_set.all %}
<h1>project: {{ project.project_name }}</h1>
{% endfor %}
{% endfor %}
(Note, you should avoid giving your ForeignKey fields names ending in "_id". The Django field refers to the entire Company, not the ID; the fields should be called just company. The underlying database will be suffixed with _id anyway. Also, you don't need to use model_name prefixes on all your fields; it will be obvious from the object they are accessed on.)
I have a site which catalogs local hikes, and users can log that they have been on the hike. I have a search page which contains the hikes, and one of the fields I'm trying to display is a list of all the people who have been on the hike. I've got this figured out within the individual detail page of the hike, but can't figure out how to create a new queryset within the queryset which is printing the hikes, in order to display this info on a search page.
Here's some code:
models.py:
class Hike(models.Model):
name = models.CharField(max_length=255, unique=True)
slug = models.SlugField(unique=True)
...
class UserLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
hike = models.ForeignKey(Hike, on_delete=models.CASCADE)
views.py:
def hike_list(request):
qs = Hike.objects.all()
... some other filters here
?-->users = UserLog.objects.filter(id=qs.id)
template:
{% for qs in qs %}
{{ hike.name }}{{ hike.other_details_and_stuff }}
?----> {% for users in hikes %}{{ user.name }}{% endfor %}
{% endfor %}
Here's the working code within the individual hike's detail page:
views.py:
def hike_detail (request, slug)
users = UserLog.objects.filter(hike__slug=slug)
How do I call on the slug from each individual item in the queryset, then run a queryset on that?
The easiest is to add a ManyToManyField to Hike:
class Hike(models.Model):
...
users = models.ManyToManyField(User, through='app.UserLog')
If you have no extra fields in UserLog, you can even remove the UserLog model and the through parameter alltogether. In the template you can do:
{% for hike in qs %}
{{ hike.name }}{{ hike.other_details_and_stuff }}
{% for user in hike.users.all %}{{ user.name }}{% endfor %}
{% endfor %}
In order avoid too many queries, you should prefetch the users in the Hike query in the view:
qs = Hike.objects.all().prefetch_related('users')
Without the ManyToManyField, you could add a property and user the same template, but the prefetch clause could not be used that easily:
class Hike(models.Model):
...
#property
def users(self):
return User.objects.filter(userlog__hike=self)
I have a Django app where users post messages and other users can vote the answers up or down, very similar to SO. I'm having an issue trying to get the "thumbs up" and "thumbs down" counts from within the template and I'm hoping someone can help me. The PostVote is a many-to-one relationship with the Post class. Here is what my model looks like:
class Post(models.Model):
account = models.ForeignKey(Account)
message = models.CharField(max_length=1024)
timestamp = models.DateTimeField('post timestamp')
class PostVote(models.Model):
post = models.ForeignKey(Post)
account = models.ForeignKey(Account)
vote = models.CharField(max_length=16, choices=VOTE_CHOICES)
timestamp = models.DateTimeField('vote timestamp')
Here is how I'm getting my posts:
posts = Post.objects.all().order_by('-timestamp')[:10]
My template looks roughly like:
{% for post in posts %}
<div>Thumbs up count: {{ WHAT_HERE }}</div>
<div>Thumbs down count: {{ WHAT_HERE }}</div>
{% endfor %}
How can I get the counts in there? I'm sure it involves 'annotate' somehow, but I'm having a difficult time coming up with this one. Any help would be greatly appreciated!
You shouldn't really be doing logic in your templates. Add a couple count methods to your Post model:
class Post(models.Model):
account = models.ForeignKey(Account)
message = models.CharField(max_length=1024)
timestamp = models.DateTimeField('post timestamp')
def upvote_count(self):
return self.postvote_set.filter(vote=VOTE_CHOICES[0][0]).count()
def downvote_count(self):
return self.postvote_set.filter(vote=VOTE_CHOICES[1][0]).count()
Then use them in your template:
{% for post in posts %}
<div>Thumbs up count: {{ post.upvote_count }}</div>
<div>Thumbs down count: {{ post.downvote_count }}</div>
{% endfor %}