I'm new to SQLAlchemy and I'm trying to create a new item that includes a list of several sub-items (simple One-to-Many relation) using Flask-RESTful and Flask-SQLAlchemy. I'm trying to create both the item and sub-items simultaneously and I'm not clear on how SQLAlchemy is supposed to work.
class ItemModel(Model):
__tablename__ = 'items'
id = Column(Integer, primary_key=True)
name = Column(String(80))
sub_items = relationship('SubItemModel')
class SubItemModel(db.Model):
__tablename__ = 'sub_items'
id = Column(Integer, primary_key=True)
item_id = Column(Integer, ForeignKey('items.id'))
name = Column(String(80))
item = relationship('ItemModel')
I want to add an item along with several sub_items (through a POST route), but I'm having trouble wrapping my head around which objects to create first and how much SQLAlchemy will do automatically. I got something working by creating an item with no sub_items, creating the sub_items, and then re-saving the item with the sub_items. But this seems pretty clunky, particularly in cases where either the item or some of the sub_items might already exist.
My intuition is just to do something like this:
item = ItemModel(
name="item1",
sub_items=[{name: "subitem1"},{name: "subitem2"}])
session.add(self)
session.commit()
But it's not working (I'm getting errors about unhashable types), and it seems ...too simple, somehow. Like I should define the sub_item objects separately. But since they depend on the item_id I'm not sure how to do this.
I'm sure this has been answered before or explained in a simple tutorial somewhere but I haven't been able to find anything simple enough for me to understand. I'm hoping someone can walk me through the basics. (which parts are supposed to be magical and which parts do I still have to code manually...)
Thanks.
The "many" side of any SQLAlchemy relationship behaves like a standard Python list. You should be creating the SubItemModel objects directly and appending them to ItemModel:
item = ItemModel(name='item1')
subitem1 = SubItemModel(name='subitem1')
subitem2 = SubItemModel(name='subitem2')
item.sub_items.append(subitem1)
item.sub_items.append(subitem2)
or, to append multiple items at once, you can use the standard list extend method:
item = ItemModel(name='item1')
subitem1 = SubItemModel(name='subitem1')
subitem2 = SubItemModel(name='subitem2')
item.sub_items.extend([subitem1, subitem2])
You can, if you want, create the subitems directly when you're adding them:
item = ItemModel(name='item1')
item.sub_items.extend([SubItemModel(name='subitem1'), SubItemModel(name='subitem2')])
Whichever option you choose, you should be adding your created item object to the session, which will automatically include the new child records you've created:
session.add(item)
session.commit()
Voila, your item and subitems should all be inserted into the DB at once.
Related
I am tracking laptimes for drivers that set fastest laps on a circuit over a period of a month. Every few days I update their latest laptimes so each month they might have many laps completed. I'm trying to build a SQL Alchemy model that returns a list of all laptimes for that month (called a challenge) but filters to the fastest times per driver.
I have this working, but it doesn't feel very pythonic. I don't know if the filtering should be part of a relationship in the model (e.g. a field that returns the list of fastest laps, per driver, per challenge) or if this is done outside of the model as I construct the view (using Flask)
Here is my model:
class Challenges(db.Model):
__tablename__ = 'challenges'
id = db.Column(db.Integer, primary_key=True)
season_id = db.Column(db.Integer, db.ForeignKey('seasons.id'))
type = db.Column(db.Text)
track_id = db.Column(db.Integer, db.ForeignKey('tracks.id'))
layout_id = db.Column(db.Integer, db.ForeignKey('layouts.id'))
car_class_id = db.Column(db.Integer, db.ForeignKey('car_classes.id'))
car_id = db.Column(db.Integer, db.ForeignKey('cars.id'))
# one to many
# laps = db.relationship('Laps', backref='challenge',lazy='dynamic')
# one to one
track = db.relationship('Tracks', backref='challenge',uselist=False)
layout = db.relationship('Layouts', backref='challenge',uselist=False)
car_class = db.relationship('Car_classes', backref='challenge',uselist=False)
car = db.relationship('Cars', backref='challenge',uselist=False)
What I'd like to do is replace the 'Laps' relationship (commented out above which returns ALL laps) with one that returns only the fastest lap per driver for that challenge. I currently have this SQL Alchemy code that runs in the Flask blueprint to get the data ready to display, but it feels like this should be inside the model as a 'leaderboard' relationship. I hope this makes sense. Here is my query.
leaderboard = db.session.query(
Laps.challenge_id,
Drivers.id,
Drivers.name,
Drivers.country,
func.min(Laps.laptime),
Laps.datetime,
Laps.verified
).join(Drivers, Laps.driver_id == Drivers.id
).filter(Laps.challenge_id == challenges[0].id
).group_by(
Laps.driver_id
).order_by(
Laps.laptime.asc()
).all()
So this DOES give me what I need, but it feels really clunky and I'm trying to learn Python the correct way and don't want to use what feels like a work around. If I simply place this code in the model it fails (Laps is not defined) and its not defined as a relationship like I think it should be.
Very new to SQLAlchemy so apologies if this is a simple error on my part. Its more important to me to do this right than simply to get it working. Thanks in advance for any expertise shared.
I am working a rest api with python flask and SQLalchemy. I have 2 classes Parent and Child:
Class Parent(db.Model):
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
name = db.Column(db.String, nullable=False)
children = relationship('Child',
secondary=parent_has_children,
back_populates='parents'
)
Class Child(db.Model):
id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True)
name = db.Column(db.String, nullable=False)
parents = relationship('Parent',
secondary=parent_has_children,
back_populates='children'
)
parent_has_children = db.Table('parent_has_children', db.metadata,
db.Column('parent_id', db.Integer, ForeignKey('Parent.id')),
db.Column('child_id', db.Integer, ForeignKey('Child.id'))
)
I have a many to many relationship and for that reason i am using a secondary table.Lets say i have a route who recieves a child_id and a parent_id and building their relationship:
#app.route('/buildrelationship', methods=['POST'])
def buildrelationship():
child_id= request.json['student_id']
parent_id = request.json['parent_id']
child = Child.query.get(child_id)
parent = Parent.query.get(parent_id)
parent.children.append(child)
db.session.commit()
This way i added relationship between parent a child but i had to get the parent and the child from database first and then add relationship.
The request.json may have a list of children to append to a parent or a list o parents to append to a particular child.In this case i have to query as many times as the length of list to take the parent or child and then append the relationship.Is there any better way to append relationship instead of load parent and child objects every time?
It's possible to reduce the querying, but you want to lean on the ORM as much as possible. If you didn't do the query to resolve the POSTed data to the Child or Parent object and instead, say, directly inserted into the M2M table the id's presented-- you could cause a bit of a headache with your databases integrity.
You only have to fire one update query-- if you first iterate once through your list of Children you could end up with a children = [Child<1>, Child<2>, Child<3>] list, then you could just Parent.children.append(children)
If, you were really dealing with tens/hundreds of thousands of child objects per POST and time, memory, etc was actually an issue, you could flip to bulk loading the data, pretty much totally skipping the ORM layer and all the safety features it's helping you with.
First you would want to get a list of your current child objects so you could make sure you're not going to cause an integrity error (again, safety gloves are off, and you're pretty odd if you're doing this without good reason).
existing = {x.id for x in Child.query.all()}
# lets say: existing = {1,3,4,5,6}
Next you'd get your list of POSTed child ids:
target = request.json['student_id']
# lets say target = [1,2,3,3,3]
So, we could now filter down on what actually needs to get inserted, cleaning up anything that might cause us trouble:
children_to_insert = {x for x in target if x in existing}
# active_children = {1,3}
Build a list of dictionaries to represent our M2M table data:
parent_id = request.json['parent_id']
bulk = [{'parent_id': parent_id, 'child_id': x} for x in children_to_insert]
# Then we'd bulk operation it into the database:
db.engine.execute(
parent_has_children.insert(bulk)
)
I've skipped all the other integrity checking you would want (does the parent exist? does it already have children?) but you hopefully get the point, which is, just use the ORM and don't try and go around it's back without a very good reason.
I'm trying to use association proxies to make dealing with tag-style records a little simpler, but I'm running into a problem enforcing uniqueness and getting objects to reuse existing tags rather than always create new ones.
Here is a setup similar to what I have. The examples in the documentation have a few recipes for enforcing uniqueness, but they all rely on having access to a session and usually require a single global session, which I cannot do in my case.
from sqlalchemy import Column, Integer, String, create_engine, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
engine = create_engine('sqlite://', echo=True)
Session = sessionmaker(bind=engine)
def _tag_find_or_create(name):
# can't use global objects here, may be multiple sessions and engines
# ?? No access to session here, how to do a query
tag = session.query(Tag).filter_by(name=name).first()
tag = Tag.query.filter_by(name=name).first()
if not tag:
tag = Tag(name=name)
return tag
class Item(Base)
__tablename__ = 'item'
id = Column(Integer, primary_key=True)
tags = relationship('Tag', secondary='itemtag')
tagnames = association_proxy('tags', 'name', creator=_tag_find_or_create)
class ItemTag(Base)
__tablename__ = 'itemtag'
id = Column(Integer, primary_key=True)
item_id = Column(Integer, ForeignKey('item.id'))
tag_id = Column(Integer, ForeignKey('tag.id'))
class Tag(Base)
__tablename__ = 'tag'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
# Scenario 1
session = Session()
item = Item()
session.add(item)
item.tagnames.append('red')
# Scenario 2
item2 = Item()
item2.tagnames.append('blue')
item2.tagnames.append('red')
session.add(item2)
Without the creator function, I just get tons of duplicate Tag items. The creator function seems like the most obvious place to put this type of check, but I'm unsure how to do a query from inside the creator function.
Consider the two scenarios provided at the bottom of the example. In the first example, it seems like there should be a way to get access to the session in the creator function, since the object the tags are being added to is already associated with a session.
In the second example, the Item object isn't yet associated with a session, so the validation check can't happen in the creator function. It would have to happen later when the object is actually added to a session.
For the first scenario, how would I go about getting access to the session object in the creator function?
For the second scenario, is there a way to "listen" for when the parent object is added to a session and validate the association proxies at that point?
For the first scenario, you can use object_session.
As for the question overall: true, you need access to the current session; if using scoped_session in your application is appropriate, then the second part of the Recipe you link to should work fine to use. See Contextual/Thread-local Sessions for more info.
Working with events and change objects when they change from transient to persistent state will not make your code pretty or very robust. So I would immediately add new Tag objects to the session, and if the transaction is rolled back, they would not be in the database.
Note that in a multi-user environment you are likely to have race condition: the same tag is new and created in simultaneously by two users. The user who commits last will fail (if you have a unique constraint on the database).
In this case you might consider be without the unique constraint, and have a (daily) procedure to clean those duplicates up (and reassign relations). With time there would be less and less new items, and less possibilities for such clashes.
Fairly new to Python and very new to SQLAlchemy. Wondering how I can use a for loop to make multiple SQLAlchemy records from a list. Here's a very simplified version of what I have.
class ListItem(Base):
__tablename__ = list_items
id = Column(Integer, primary_key=True)
item = Column(String(50))
def list_to_ListItem(list_of_things):
for thing in list_of_things:
listitem = ListItem()
listitem.item = thing
I haven't yet run this because there's actually a lot more to my code that still needs to be worked out, but I'm under the impression that, instead of creating a record in list_items for each thing in list_of_things, this will simply create one record assigned to listitem that it will overwrite with every iteration of the for loop.
So how do I do this? Like I said, I'm fairly new to Python, but I've heard something about factory functions that I don't understand but which, at least in name, sounds promising for this problem. Thanks in advance!
List comprehension?
list_items = [ListItem(item=thing) for thing in list_of_things]
Is it possible to add to a SQLAlchemy relationship using ids rather than objects?
For example, consider two declarative SQLAlchemy classes, Review and Artist, with a relationship between them:
class Review(Base):
artist_id = Column(Integer, ForeignKey('artist.id'))
artist = relationship(Artist, backref=backref('reviews', order_by=id))
# etc.
class Artist(Base):
# etc.
With a list of review ids to add to an artist, I seem to need to look up the artist from the id, then add the artist object to the review, like this:
for review_id in review_ids:
review = session.query(Review).filter(Review.id==review_id).first()
artist.reviews.append(review)
I'm sure it would be more efficient to skip the lookup and just add the ids, but is this possible?
Your best bet is probably to compose an update expression against the backing tables. Otherwise, you can't really modify a Review without actually querying for it (and that's what your doing; you aren't actually modifying the Artist at all).
Assuming Review.id is the primary key, That would roughly be:
conn = session.connection()
conn.execute(Review.__table__
.update()
.values(artist_id=artist_id)
.where(Review.id.in_(review_ids))
)
I think if you want to stick with the pure ORM solution, you'll have to live with the somewhat inefficient querying pattern.
#TokenMacGuy has provided a good alternative. You could integrate that approach by adding add_reviews() to the Artist() class. This would augment the 'standard' orm api, so you could use either depending on the situation.
from sqlalchemy.orm.session import object_session
class Artist(Base):
def add_reviews(self, review_ids):
sess = object_session(self)
if isinstance(review_ids, int): review_ids = [review_ids]
sess.execute(
Review.__table__
.update()
.values(artist_id=self.artist_id)
.where(Review.id.in_(review_ids))
)
sess.refresh(self)
review_ids = [123, 556, 998, 667, 111]
artist.add_reviews(review_ids)
review_id = 333
artist.add_reviews(review_id)
I'm not sure if I understood. I guess you want something like this:
reviews = session.query(Review).filter(Review.id.in_(review_ids)).all()
artist.reviews.extend(reviews)