Flask Sqlalchemy - query many to many with several values - flask

I have a many-to-many relationship between matches and teams, where more than one team can play in a match and each team can have more than one match. The models are:
class Match(db.Model):
__tablename__ = 'match'
id = Column(
db.Integer,
primary_key=True,
autoincrement=True)
teams = db.relationship('Team', secondary='team_match_link')
class Team(db.Model):
__tablename__ = 'team'
id = Column(
db.Integer,
primary_key=True,
autoincrement=True)
matches = db.relationship('Match', secondary='team_match_link')
class TeamMatchLink(db.Model):
__tablename__ = 'team_match_link'
match_id = Column(
db.Integer,
db.ForeignKey('match.id'),
primary_key=True
)
team_id = Column(
db.Integer,
db.ForeignKey('team.id'),
primary_key=True
)
Given two teams [T1, T2], how do I query for the matches that contain EXACTLY these two teams and no other teams?
This gives me the matches that contain at least the two teams and then I could check if there are other teams in these matches of course. But it looks ugly and I'm sure there is a better/more effcient way? Ideally the solution should work for n teams without sending me to loop hell.
res1 = match.query.filter(match.team.any(id=T1.id)).all()
res2 = match.query.filter(match.team.any(id=T2.id)).all()
res = [i for i in res1 if i in res2]

How about querying the match team links and aggregate by counting the number of entries.
Query and some explanations below:
your_teams = [T1.id, T2.id]
result = db.session.query(Match, TeamMatchLink).filter(TeamMatchLink.team_id.in_(your_teams)).group_by(TeamMatchLink.match_id).having(func.count(TeamMatchLink.team_id) == len(your_teams)).join(Match).all()
The query consists of several parts and ideas strung together:
team_link_query = db.session.query(TeamMatchLink).filter(TeamMatchLink.team_id.in_(your_teams)))
This is selecting all team links that have your desired teams in them.
applicable_matches_query = team_link_query.group_by(TeamMatchLink.match_id).having(func.count(TeamMatchLink.team_id) == len(your_teams))
This aggregates the team match links to find those that have all the teams in them that you require. (A match will appear nr of times that a team link will match the your_teams list)
Finally, a join will get you the matches. You could of course reduce the query to give you only match ids from the team match link table directly.

Related

Annotate QuerySet using raw SQL

In case I am asking the wrong question, let me first state the end goal: I need to allow users to filter a ListView by a field that's not in the primary model (Salesleadquote), but instead the field comes from a model (Salesleadbusinessgroup) with a FK to a related model (Saleslead).
The way I am trying to approach this is by annotating a field on Salesleadquote.
The models:
class Salesleadquote(models.Model):
salesleadquoteid = models.AutoField(db_column='SalesLeadQuoteId', primary_key=True)
salesleadid = models.ForeignKey(Saleslead, models.DO_NOTHING, db_column='SalesLeadId')
...
class Saleslead(models.Model):
salesleadid = models.AutoField(db_column='SalesLeadId', primary_key=True)
...
class Salesleadbusinessgroup(models.Model):
salesleadbusinessgroupid = models.AutoField(db_column='SalesLeadBusinessGroupId', primary_key=True)
salesleadid = models.ForeignKey(Saleslead, models.DO_NOTHING, db_column='SalesLeadId')
businessgroupid = models.ForeignKey(Businessgroup, models.DO_NOTHING, db_column='BusinessGroupId')
The desired result (queryset), in SQL:
SELECT slq.*, slbg.BusinessGroupId FROM crm.SalesLeadQuote slq
LEFT JOIN
(SELECT SalesLeadId, BusinessGroupId
FROM crm.SalesLeadBusinessGroup ) slbg
ON slbg.SalesLeadId = slq.SalesLeadId
WHERE slbg.BusinessGroupId IN (5,21)
I know I can get a RawQuerySet by doing something like
Salesleadquote.objects.raw("SELECT salesleadquote.*, \
salesleadbusinessgroup.businessgroupid \
FROM salesleadquote \
LEFT JOIN salesleadbusinessgroup \
ON salesleadquote.salesleadid = salesleadbusinessgroup.salesleadid \
WHERE salesleadbusinessgroup.businessgroupid IN (5,21)")
But I need the functionality of a QuerySet, so my idea was to annotate the desired field (businessgroupid) in Salesleadquote, but I've been struggling with how to accomplish this.
I implemented a work-around that doesn't address my original question but works for my use case. I created a view at the database level (called SalesLeadQuoteBG) using the SQL I had posted and then tied that to a model to use with Django's ORM.
class Salesleadquotebg(models.Model):
"""
This model represents a database view that extends the Salesleadquote table with a business group id column.
There can be multiple business groups per quote, resulting in duplicate quotes, but this is handled at the view and template layer
via filtering (users are required to select a business group).
"""
salesleadquoteid = models.IntegerField(db_column='SalesLeadQuoteId', primary_key=True) # Field name made lowercase.
salesleadid = models.ForeignKey(Saleslead, models.DO_NOTHING, db_column='SalesLeadId') # Field name made lowercase.
...
businessgroupid = models.ForeignKey(Businessgroup, models.DO_NOTHING, db_column='BusinessGroupId')
I am using django-filters for the filtering.
filters.py:
BG_CHOICES = (
(5, 'Machine Vision'),
(21, 'Process Systems'),
)
class BGFilter(django_filters.FilterSet):
businessgroupid = django_filters.ChoiceFilter(choices=BG_CHOICES)
class Meta:
model = Salesleadquotebg
fields = ['businessgroupid', ]
This can be done with pure Django ORM. Relationships can be followed backwards (filtering on salesleadbusinessgroup) and the double underscore syntax can be used to query attributes of the related model and also follow more relationships
Salesleadquote.objects.filter(
salesleadbusinessgroup__businessgroupid__in=(5,21)
)

Django querying many to many relations

I have 3 models: group, word, phrase
As you can see word belongs to group and there is a many to many relation between phrases and words:
class Group(models.Model):
name = models.TextField()
class Word(models.Model):
group = models.ForeignKey(Group, on_delete=models.SET_NULL, null=True,
related_name = "words")
class Phrase(models.Model):
words = models.ManyToManyField(Word, null=True,related_name = "phrases")
name = models.TextField()
And right now I need to find all phraseswith name ="xxx"
that have at least one word from group with group id = '177'
How do I do this?
Phrase.objects.filter(name="xxx", words__group__id=177)
You can filter all phrases with name "xxx" and one of words' group_id equals 177.
You can do this with a filter that both filters on the name field, and the words__group_id=177:
Phrase.objects.filter(
name='xxx',
words__group_id=177
).distinct()
We here thus use a JOIN on the words and the phrase_words table, and then filter such that the group_id of the Word is 177.
The .distinct() can be important, since if mutliple words are in the group with id 177, then that Phrase would be repeated multiple times.
This works in case id is the primary key of the Group. If you defined another column named id, and that column is not the primary key (which is quite uncommon), we can use:
Phrase.objects.filter(
name='xxx',
words__group__id=177
).distinct()
Mind that here we use two consecutive underscores (__) instead of one (_) to look "through" the foreign key. In this case, this will result in an extra JOIN at the database level, and therfore can be a bit less efficient.

Django query of table with implicit join on itself

I've read the documentation and looked at other questions posted here, but I can't find or figure out whether this is possible in Django.
I have a model relating actors and movies:
class Role(models.Model):
title_id = models.CharField('Title ID', max_length=20, db_index=True)
name_id = models.CharField('Name ID', max_length=20, db_index=True)
role = models.CharField('Role', max_length=300, default='?')
This is a single table that has pairs of actors and movies, so given a movie (title_id), there's a row for each actor in that movie. Similarly, given an actor (name_id), there's a row for every movie that actor was in.
I need to execute a query to return the list of all title_id's that are related to a given title_id by a common actor. The SQL for this query looks like this:
SELECT DISTINCT r2.title_id
FROM role as r1, role as r2
WHERE r1.name_id = r2.name_id
AND r1.title_id != r2.title_id
AND r1.title_id = <given title_id>
Can something like this be expressed in a single Django ORM query, or am I forced to use two queries with some intervening code? (Or raw SQL?)
Normally I would break this into Actor and Movie table to make it easier to query, but your requirement is there so I will give it a go
def get_related_titles(title_id)
all_actors = Role.objects.filter(title_id=title_id).values_list('pk', flat=True)
return Role.objects.filter(pk__in=all_actors).exclude(title_id=title_id) # maybe u need .distinct() here
this should give you one query, validate it this way:
print(get_related_titles(some_title_id).query)

How to ask the Django manytomany manager to match several relations at once?

I've got this model:
class Movie(models.Model):
# I use taggit for tag management
tags = taggit.managers.TaggableManager()
class Person(models.Model):
# manytomany with a intermediary model
movies = models.ManyToManyField(Movie, through='Activity')
class Activity(models.Model):
movie = models.ForeignKey(Movie)
person = models.ForeignKey(Person)
name = models.CharField(max_length=30, default='actor')
And I'd like to match a movie that has the same actors as another one. Not one actor in common, but all the actors in common.
So I don't want this:
# actors is a shortcut property
one_actor_in_common = Movie.object.filter(activities__name='actor',
team_members__in=self.movie.actors)
I want something that would make "Matrix I" match "Matrix II" because they share 'Keanu Reeves' and 'Laurence Fishburne', but not match "Speed" because they share 'Keanu Reeves' but not 'Laurence Fishburne'.
The many to manytomany manager cannot match several relations at once. Down at the database level it all comes down to selecting and grouping.
So naturally speaking the only question the database is capable to answer is this: List acting activites that involve these persons, group them by movie and show only those movies having the same number of those acting activities as persons.
Translated to ORM speak it looks like this:
actors = Person.objects.filter(name__in=('Keanu Reaves', 'Laurence Fishburne'))
qs = Movie.objects.filter(activity__name='actor',
activity__person__in=actors)
qs = qs.annotate(common_actors=Count('activity'))
all_actors_in_common = qs.filter(common_actors=actors.count())
The query this produces is actually not bad:
SELECT "cmdb_movie"."id", "cmdb_movie"."title", COUNT("cmdb_activity"."id") AS "common_actors"
FROM "cmdb_movie"
LEFT OUTER JOIN "cmdb_activity" ON ("cmdb_movie"."id" = "cmdb_activity"."movie_id")
WHERE ("cmdb_activity"."person_id" IN (SELECT U0."id" FROM "cmdb_person" U0 WHERE U0."name" IN ('Keanu Reaves', 'Laurence Fishburne'))
AND "cmdb_activity"."name" = 'actor' )
GROUP BY "cmdb_movie"."id", "cmdb_movie"."title", "cmdb_movie"."id", "cmdb_movie"."title"
HAVING COUNT("cmdb_activity"."id") = 2
I also have a small application that I used to test this, but I don't know wether someone needs it nor where to host it.

Querying for exact match of objects with many to many relationship in Django

I want to write a query that matches on several objects that have manytomany relationships.
To be more clear:
I have 2 models that have many to many relationship with each other.
class Recipe(models.Model):
dish = models.IntField()
ingredients = models.ManyToManyField(Ingredient)
class Ingredient(models.Model):
matching_ingredients = models.ManyToManyField(Recipe)
plate = models.CharField(max_length=25)
my code:
firstfruit = Fruit(dish_id=1234, name='apple')
secondfruit = Fruit(dish_id=1234, name='orange')
salad = Ingredient()
salad.matching_ingredients.add(firstfruit, secondfruit)
matching_ingredients_from_dish = Recipe.objects.all()
I tried this but this will return true if anything matches:
Recipe.objects.get(matching_ingredients=matching_ingredients_from_dish)
I want to see if every matching_ingredients matches with every matching_ingredients_from_dish like an exact match.