I'm trying to filter upon a relationship. In my db I've got awards and awards categories. I want retrieve only the featured and not nominated awards.
This is my model:
class AwardsAwardsCategory(Base):
__tablename__ = 'awards_awards_categories'
id = Column(Text, primary_key=True, default=generate_unique_id)
awards = relationship("Award")
award_id = Column(Text, ForeignKey("awards.id", ondelete='cascade'))
nomination = Column(Boolean)
def __json__(self, request):
return {
"id": self.id,
"nomination": self.nomination
}
class Award(Base):
__tablename__ = 'awards'
id = Column(Text, primary_key=True, default=generate_unique_id)
type = Column(Text)
featured = Column(Integer)
awards = relationship("AwardsAwardsCategory", lazy='joined')
def __json__(self, request):
return {
"id": self.id,
"type": self.type,
"number_of_awards": len(self.awards),
"featured": self.featured,
}
This is the call I make:
query = self.session.query(Award)
query = query.join(AwardsAwardsCategory.awards)
query = query.filter(Award.featured != 0)
query = query.filter(AwardsAwardsCategory.nomination != True)
q_results = query.all()
This results in the following query:
SELECT awards.id AS awards_id, awards.type AS awards_type, awards.featured AS awards_featured, awards_awards_categories_1.id AS awards_awards_categories_1_id, awards_awards_categories_1.award_id AS awards_awards_categories_1_award_id, awards_awards_categories_1.nomination AS awards_awards_categories_1_nomination
FROM awards_awards_categories JOIN awards ON awards.id = awards_awards_categories.award_id LEFT OUTER JOIN awards_awards_categories AS awards_awards_categories_1 ON awards.id = awards_awards_categories_1.award_id
WHERE awards.featured != 0 AND awards_awards_categories.nomination != true
It is almost correct except the WHERE clause is missing a condition:
AND awards_awards_categories_1.nomination != true
How can I change my code so that it adds the last condition to the WHERE clause.
I ended up filtering on the nomination on the application layer. It's nasty, but it works.
query = self.session.query(Award)
query = query.join(AwardsAwardsCategory.awards)
query = query.filter(Award.featured != 0)
query = query.filter(AwardsAwardsCategory.nomination != True)
q_results = query.all()
# remove all objects from the session in order to keep them in the db.
self.session.expunge_all()
for award_category in q_results:
# keep a separate list of the awards, in order to keep the iteration going as desired
awards = list(award_category)
for award in award_category.awards:
if award.nomination:
awards.remove(award)
def __json__(self, request):
return {
"id": self.id,
"type": self.type,
"number_of_awards": self.number_of_awards,
"featured": self.featured,
}
award_category.number_of_awards = len(awards)
award_category.__json__ = types.MethodType( __json__, a )
If anyone knows a how to do it better in SQLAlchemy please tell me!
Related
I am making a to-do list webapp as a hobby project using flask for the backend and a PostgreSQL database to store the data. The database model is a as follows:
models.py
class Group(db.Model):
__tablename__ = "groups"
group_id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String(20), unique=True)
class Collection(db.Model):
__tablename__ = "collections"
collection_id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String(20), unique=True)
group_name = db.Column(db.String(20), db.ForeignKey("groups.name"), nullable=True)
def to_dict(self):
return {
"collection_id": self.collection_id,
"name": self.name,
"group_name": self.group_name,
}
class Task(db.Model):
__tablename__ = "tasks"
task_id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
text = db.Column(db.String(200))
completed = db.Column(db.Boolean, default=False)
collection_name = db.Column(
db.String(20), db.ForeignKey("collections.name"), nullable=False
)
def to_dict(self):
return {
"task_id": self.task_id,
"text": self.text,
"completed": self.completed,
"collection_name": self.collection_name,
}
While making the REST api logic for adding tasks to the database, I was unsure if:
I should check if the collection_name column already exists in the collections table before trying to insert the data in the database.
I should try inserting the row anyway and catch the sqlalchemy.exc.IntegrityError exception if it happens.
The problem I see with the first solution, is I need to query the collections tables for the list of valid collection_name each time I want to add a task, which I am not sure if it's a good practice performance wise.
While the problem I see with the second solution is that the sqlalchemy.exc.IntegrityError exception is pretty vague and in a more sophisticated table with several foreign keys, I would need to parse the exception's message to know which foreign key was violated.
For now, I implemented the second solution because I have a very simple table with only one foreign key constraint.
In the following you can see the code for the controller.py that handles the API call and service.py that talks with the database.
controller.py
#tasks_api.route(COMMON_API_ENDPOINT + "/tasks", methods=["POST"])
def add_task():
request_body = request.get_json()
# Check for missing fields in the call
mandatory_fields = set(["text", "collection_name"])
try:
missing_fields = mandatory_fields - set(request_body.keys())
assert len(missing_fields) == 0
except AssertionError:
return (
jsonify(
{
"error": "The following mandatory fields are missing: "
+ str(missing_fields)
}
),
400,
)
# Try to call the add task service function
try:
task = TaskService.add_task(
text=request_body["text"], collection_name=request_body["collection_name"]
)
except CollectionNotFoundError as e:
return jsonify({"error_message": str(e)}), 400
else:
return (
jsonify(
{
"result": "A new task was created successfully.",
"description": task.to_dict(),
}
),
201,
)
service.py
def add_task(text: str, collection_name: str) -> Task:
try:
with get_session() as session:
task = Task(text=text, collection_name=collection_name)
session.add(task)
return task
except sqlalchemy.exc.IntegrityError:
raise CollectionNotFoundError(
"Foreign key violation: There is no collection with the name "
+ collection_name
)
While writing this post, I wondered if this is an XY problem where both solutions are not the best. I am open to other suggestions too.
Thanks !
Here is my tables.
class maindevotee(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(225))
phonenumber = db.Column(db.String(225))
gothram = db.Column(db.String(225))
date = db.Column(db.String(50))
address = db.Column(db.String(250))
def json(self):
return {'id': self.id, 'name':self.name, 'phonenumber': self.phonenumber, 'gothram': self.gothram,
'date': self.date, 'address': self.address}
class relatives(db.Model):
id = db.Column(db.Integer, primary_key=True)
main_id = db.Column(db.Integer, db.ForeignKey('maindevotee.id'), nullable=False)
name = db.Column(db.String(225))
star = db.Column(db.String(225))
gender = db.Column(db.String(45))
relation = db.Column(db.String(45))
def json(self):
return {'main_id': self.main_id, 'name': self.name, 'star':self.star,
'gender': self.gender, 'relation': self.relation}
class services(db.Model):
id = db.Column(db.Integer, primary_key=True)
main_id = db.Column(db.Integer, db.ForeignKey('maindevotee.id'), nullable=False)
pooja = db.Column(db.String(225))
god = db.Column(db.String(225))
price = db.Column(db.Float)
donation = db.Column(db.String(225))
booking_fromdate = db.Column(db.String(50))
booking_todate = db.Column(db.String(50))
prasadam = db.Column(db.String(225))
def json(self):
return {'main_id': self.main_id, 'pooja': self.pooja, 'god': self.god,
'price': self.price, 'donation': self.donation, 'booking_fromdate': self.booking_fromdate,
'booking_todate': self.booking_todate, 'prasadam': self.prasadam}
How to get data from multiple tables in a single request. Here is my scource code to join the three tables.
If i am try to get data from database it will raise an error.
and the error is AttributeError: 'result' object has no attribute 'get_data'
can i get the data from database using foreign key.
data = db.session.query(maindevotee, relatives, services)\
.filter(maindevotee.phonenumber == 3251469870)\
.join(relatives, maindevotee.id == relatives.main_id)\
.join(services, maindevotee.id == services.main_id)\
.first()
def get_data():
return [data.json(get) for get in data.query.all()]
#app.route('/getdata/<phonenumber>',methods=['GET'])
def getdata():
return jsonify({'Devotee list': data.get_data()})
Correct
data = db.session.query(maindevotee, relatives, services)\
.filter(maindevotee.phonenumber == 3251469870)\
.join(relatives, maindevotee.id == relatives.main_id)\
.join(services, maindevotee.id == services.main_id)\
.first()
to
data = db.session.query(maindevotee, relatives, services)\
.filter(
(maindevotee.phonenumber == '3251469870')
& (maindevotee.id == relatives.main_id)
& (maindevotee.id == services.main_id)
).first()
for more clarifications, ask in the comments.
Upon comment
in
#app.route('/getdata/<phonenumber>',methods=['GET'])
def getdata():
return jsonify({'Devotee list': data.get_data()})
data contains the query results, that do not include the function get_data(), therefore you face the mentioned error.
Try the following modification, I think this is the result form you may want:
#app.route('/getdata/<phonenumber>',methods=['GET'])
def getdata():
return jsonify({**data.maindevotee.json(),**data.relatives.json(),**data.services.json()})
Good Luck
I'm creating two tables and linking them using SQLAlchemy relationships (using SQLite as my DB)
class Album(Base):
__tablename__ = 'albums'
id = Column(INTEGER, primary_key=True, unique=True, autoincrement=True, nullable=False)
name = Column(TEXT)
tracks = relationship('Track', back_populates='albums')
class Track(Base):
__tablename__ = 'tracks'
id = Column(INTEGER, primary_key=True, unique=True, autoincrement=True, nullable=False)
name = Column(TEXT)
albums = relationship('Album', back_populates='tracks')
def insert_data(metadata, path, ext):
session = get_session() # returns SQLAlchemy session
tracks = get_or_create(session,
Track,
name=metadata['title']
)
_ = get_or_create(session,
Album,
name=metadata['album'],
tracks=tracks
)
def get_instance(session, model, permutation):
try:
return session.query(model).filter_by(**permutation).first()
except NoResultFound:
return None
def create_instance(session, model, permutation):
try:
instance = model(**permutation)
session.add(instance)
session.flush()
except Exception as msg:
log.error(f'model:{model}, args:{permutation} -> msg:{msg}')
session.rollback()
raise msg
return instance
def get_or_create(session, model, **metadata):
data_permutations = [dict(zip(metadata, value)) for value in product(*metadata.values())]
ret = []
for permutation in data_permutations:
instance = get_instance(session, model, permutation)
if instance is None:
instance = create_instance(session, model, permutation)
ret.append(instance)
session.commit()
return ret
insert_metadata(metadata, path, ext)
metadata looks like this:
{
'name': ['foo'],
'data': ['bar', 'baz'],
...
}
It can have an unlimited amount of keys, that can have lists of any length as values. Therefore I create all possible outcomes (permutations) of this data and save it as a list of unique dicts, like this:
[{'name': 'foo', 'data': 'bar'}, {'name': 'foo', 'data': 'baz'}]
Now, when I call insert_data, I'm getting the following message:
sqlalchemy.exc.NoForeignKeysError: Can't find any foreign key relationships between 'albums' and 'tracks'.
Traceback shows that the function that's throwing the exception is get_instance. I'm suspecting it has something to do with my query, since I double-checked table creation against both the documentation and other Stack Overflow questions, and the syntax seems to be correct.
How do I need to alter my query (see the try block in get_instance) so the program doesn't crash? Or is the error elsewhere?
The problem was that the table 'tracks' was missing the column album_id:
album_id = Column(INTEGER, ForeignKey('albums.id'))
I apologize in advanced for any lack of explanation, as well as the length of this post. I think the issue is much more simple than I'm making it out to be. I have two models utilizing a one to many relationship. For my InsightModel, I have the json() method displaying the following:
{
name: "insightname",
start: 1,
end: 3,
podcast_id: 1,
podcast: {
name: "podcast1",
wave_data: 1,
length: 2,
host: "Hosterman",
category: "entertain",
pub_date: "11/1",
cover_art_url: "google.com"
}
}
And for my PodcastModel, the json() method displays the following:
{
name: "podcast1",
wave_data: 1,
length: 2,
host: "Hosterman",
category: "entertain",
pub_date: "11/1",
cover_art_url: "google.com",
insights: [
{
name: "insightname",
start: 1,
end: 3,
podcast_id: 1
}
]
}
This works as I need it to, but in order to make it work, I had to create two json() methods for each class, in order to avoid recursion in the PodcastModel that would look like the following:
{
name: "podcast1",
wave_data: 1,
length: 2,
host: "Hosterman",
category: "entertain",
pub_date: "11/1",
cover_art_url: "google.com",
insights: [
{
name: "insightname",
start: 1,
end: 3,
podcast_id: 1,
podcast: {
name: "podcast1",
wave_data: 1,
length: 2,
host: "Hosterman",
category: "entertain",
pub_date: "11/1",
cover_art_url: "google.com",
}
}
]
}
My code for the PodcastModel is:
from db import db
from datetime import datetime
class PodcastModel(db.Model):
__tablename__ = 'podcasts'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
wave_data = db.Column(db.Float(precision=2))
length = db.Column(db.Float(precision=2))
host = db.Column(db.String(80))
category = db.Column(db.String(80))
pub_date = db.Column(db.String(50))
cover_art_url = db.Column(db.String(200))
insights = db.relationship('InsightModel', backref='podcast', lazy='dynamic')
def __init__(self, name, wave_data, length, host, category, pub_date, cover_art_url):
self.name = name
self.wave_data = wave_data
self.length = length
self.host = host
self.category = category
self.pub_date = pub_date
self.cover_art_url = cover_art_url
def json(self):
return {'name': self.name, 'wave_data': self.wave_data, 'length': self.length, 'host': self.host, 'category': self.category, 'pub_date': self.pub_date, 'cover_art_url': self.cover_art_url, 'insights': [insight.json_no_podcast() for insight in self.insights.all()]}
def json_no_insight(self):
return {'name': self.name, 'wave_data': self.wave_data, 'length': self.length, 'host': self.host, 'category': self.category, 'pub_date': self.pub_date, 'cover_art_url': self.cover_art_url}
#classmethod
def find_by_name(cls, name):
# Select * FROM items WHERE name=name LIMIT 1
return cls.query.filter_by(name=name).first()
#classmethod
def find_by_id(cls, _id):
return cls.query.filter_by(id=_id)
And the InsightModel is the following:
from db import db
from models.podcast import PodcastModel
class InsightModel(db.Model):
__tablename__ = 'insights'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
start = db.Column(db.Float(precision=2))
end = db.Column(db.Float(precision=2))
podcast_id = db.Column(db.Integer, db.ForeignKey('podcasts.id'))
#podcast = db.relationship('PodcastModel')
def __init__(self, name, start, end, podcast_id):
self.name = name
self.start = start
self.end = end
self.podcast_id = podcast_id
def json(self):
podcast = PodcastModel.find_by_id(self.podcast_id).first().json_no_insight()
return {'name': self.name, 'start': self.start, 'end': self.end,
'podcast_id': self.podcast_id, 'podcast': podcast}
def json_no_podcast(self):
return {'name': self.name, 'start': self.start, 'end': self.end,
'podcast_id': self.podcast_id}
#classmethod
def find_by_name(cls, name):
# Select * FROM items WHERE name=name LIMIT 1
return cls.query.filter_by(name=name).first()
As you can see, I added the json_no_insights() and json_no_podcast() methods to prevent recursion from happening. However, I'm sure reading this code has already given you a pitted feeling in your stomach and I'm desperate for a better way to write it. Thank you very much for any insight and once again, I apologize the for the length of this post or any lack of explanation.
Make your life easier - use marshmallow.
from marshmallow import Schema, fields
from flask import jsonify
class PodcastSchema(Schema):
name = fields.Str()
wave_data = fields.Float()
length = fields.Float()
host = fields.Str()
category = fields.Str()
pub_date = fields.Str()
cover_art_url = fields.Str()
insights = fields.Nested('InsightSchema')
class InsightSchema(Schema):
name = fields.Str()
start = fields.Float()
end = fields.Float()
podcast_id = fields.Integer()
Then simply dump your data like this:
podcast_schema = PodcastSchema() # for dict (single)
podcasts_schema = PodcastSchema(many=True) # for list (array)
jsonify(podcast_schema.dumps(your_json)
Notice lack of a podcast field in the PodcastSchema - that would cause (without tweaking) an infinite recursion. In case you would need that field, you might try as follows:
class PodcastSchema(Schema):
name = fields.Str()
wave_data = fields.Float()
length = fields.Float()
host = fields.Str()
category = fields.Str()
pub_date = fields.Str()
cover_art_url = fields.Str()
# dump insights without podcast field
insights = fields.Nested('InsightSchema', exclude=('podcast', ))
class InsightSchema(Schema):
name = fields.Str()
start = fields.Float()
end = fields.Float()
podcast = fields.Nested('PodcastSchema')
I've created models for my database:
class Album(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128))
year = db.Column(db.String(4))
tracklist = db.relationship('Track', secondary=tracklist,
backref=db.backref('albums',
lazy='dynamic'), lazy='dynamic')
class Track(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128))
class Artist(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
releases = db.relationship('Track', secondary=releases,
backref=db.backref('artists',
lazy='dynamic'), lazy='dynamic')
They are many-to-many related Album <--> Track <--> Artist
Next, I have this form:
class SearchForm(FlaskForm):
search_by_album = StringField('Album', validators=[Optional()])
search_by_artist = StringField('Artist', validators=[Optional()])
search_track = StringField('Track', validators=[Optional()])
year = StringField('Year', validators=[Optional(), Length(max=4)])
My idea is to give the user freedom in filling desired combination of forms (but at least one is required), so I've got this function, which recieves SearchForm().data (an immutable dict 'field_name': 'data'):
def construct_query(form):
query = db.session.query(*[field.label.text for field in form if field.data and field.name != 'csrf_token'])
if form.search_by_album.data:
query = query.filter(Album.title == form.search_by_album.data)
if form.search_by_artist.data:
query = query.filter(Artist.name == form.search_by_artist.data)
if form.search_track.data:
query = query.filter(Track.title == form.search_track.data)
if form.year.data:
query = query.filter(Album.year == form.year.data)
result = query.all()
return result
My question is if there is a more abstract way of adding filters in the function above? If one day I decide to add more columns to my tables (or even create new tables), I will have to add more monstrous ifs to constrcut_query(), which will eventually grow enormous. Or such an abstractions is not a pythonic way because "Explicit is better than implicit"?
PS
I know about forms from models, but I don't think that they are my case
One way would be associating the filter-attribute with the fields at some place, e.g. as a class attribute on the form itself:
class SearchForm(FlaskForm):
search_by_album = StringField('Album', validators=[Optional()])
search_by_artist = StringField('Artist', validators=[Optional()])
search_track = StringField('Track', validators=[Optional()])
year = StringField('Year', validators=[Optional(), Length(max=4)])
# map form fields to database fields/attributes
field_to_attr = {search_by_album: Album.title,
search_by_artist: Artist.name,
search_track: Track.title,
year: Album.year}
When building the query, you could then build the where clause in a pretty comfortable way:
def construct_query(form):
query = db.session.query(*[field.label.text for field in form if field.data and field.name != 'csrf_token'])
for field in form:
if field.data:
query = query.filter(form.field_to_attr[field] == field.data)
# or:
# for field, attr in form.field_to_attr.items():
# if field.data:
# query = query.filter(attr == field.data)
result = query.all()
return result
Adding new fields and attributes to filter on would then only translate to the creating the field and its mapping to an attribute.