How to update many to many relationship by using id with sqlalchemy? - python

I know I can simply update many to many relationship like this:
tags = db.Table('tags',
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
db.Column('page_id', db.Integer, db.ForeignKey('page.id'), primary_key=True)
)
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
tags = db.relationship('Tag', secondary=tags, lazy='subquery',
backref=db.backref('pages', lazy=True))
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag1 = Tag()
tag2 = Tag()
page = Page( tags=[tag1])
and later for updating:
page.append(tag2)
but I want to update them only by the tag id, Assume I have to create a general function that only accepts person and ids for addresses and update it.
What I want is something like this:
page = Page(tags=[1,2]) # 1 and 2 are primary keys of (tag)s
or in a function
def update_with_foreignkey(page, tags=[1,2]):
# dosomething to update page without using Tag object
return updated page

It was a little tricky and by using the evil eval but finally, I find a general way to update many to many relations using foreign keys. My goal was to update my object by getting data from a PUT request and in that request, I only get foreign keys for the relationship with other data.
Step by step solution:
1- Find relationships in the object, I find them using __mapper__.relationships
2- Find the key that represents the relationship.
for rel in Object.__mapper__.relationships:
key = str(rel).rsplit('.',1)[-1]
in question case it return 'tags' as the result.
3- Find the model for another side of the relation ( in this example Tag).
3-1 Find name of the table.
3-2 Convert table name to camleCase because sqlalchemy use underscore for the table name and camelCase for the model.
3-3 Use eval to get the model.
if key in data:
table = eval(convert_to_CamelCase(rel.table.name))
temp = table.query.filter(table.id.in_(data[key])).all() # this line convert ids to sqlacemy objects
All together
def convert_to_CamelCase(word):
return ''.join(x.capitalize() or '_' for x in word.split('_'))
def update_relationship_withForeingkey(Object, data):
for rel in Object.__mapper__.relationships:
key = str(rel).rsplit('.',1)[-1]
if key in data:
table = eval(convert_to_CamelCase(rel.table.name))
temp = table.query.filter(table.id.in_(data[key])).all() # this line convert ids to sqlacemy objects
data[key] = temp
return data
data is what I get from the request, and Object is the sqlalchemy Model that I want to update.
running this few lines update give me the result:
item = Object.query.filter_by(id=data['id'])
data = update_relationship_withForeingkey(Object,data)
for i,j in data.items():
setattr(item,i,j)
db.session.commit()
I'm not sure about caveats of this approach but it works for me. Any improvement and sugesstion are welcome.

Related

What's the proper way of handling business logic in Flask SQLALchemy models?

I'm wondering what's the proper way of retrieving data from the model. Let's take the classes below for the example:
class A(db.Model):
def get_attributes():
return self.product_category.attributes
class Attribute(db.Mdel):
attribute_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
label = db.Column(db.String(255), nullable=False)
Let's say that calling get_attributes() returns three objects of the Attribute class.
In my route, I only want to receive the list of attribute labels. What I'm currently doing is looping through the objects and retrieve the label property like this:
labels = [i.label for i in obj.get_attributes()]
Which I don't think is a proper way of doing it. Is there any better way to achieve this?
You should use relationship between A and Attribute.
For example
class A(db.Model):
# ...
# table primary key, columns etc.
# ...
attributes = db.relationship(
"Attribute",
backref="as",
lazy=True
)
class Attribute(db.Model):
# ...
# table primary key, columns etc.
# ...
a_id = db.Column(db.Integer, db.ForeignKey('a.id'))
After that, you can access certain "a" attributes with a.attributes. To add an attribute to "a" just: a.attributes.append(a)

SQLAlchemy Many-To-One relation query filter for column in list

I have following models defined:
class Attribute(Base):
__tablename__ = "attributes"
id = Column(BigInteger, primary_key=True, index=True)
data_id = Column(BigInteger, ForeignKey("data.art_no"))
name = Column(VARCHAR(500), index=True)
data = relationship("Data", back_populates="attributes_rel")
class Data(Base):
__tablename__ = "data"
art_no = Column(BigInteger, primary_key=True, index=True)
multiplier = Column(Float)
attributes_rel = relationship("Attribute", back_populates="data", cascade="all, delete, delete-orphan")
#property
def attributes(self):
return [attribute.name for attribute in self.attributes_rel]
If I query for Data rows, I get this rows (only attributes property:
#1 ['attributeX', 'attributeY']
#2 ['attributeZ']
#3 ['attributeX', 'attributeZ']
I want to do following thing now:
I have this list ['attributeX'] and I want to query my data and only get the Data rows back, which has the 'attributeX' attribute.
If I have this list ['attributeX', 'attributeZ'], I want to query my data and only get the Data rows back, which has the 'attributeX' AND 'attributeZ' attribute.
How can I do the queries?
I tried .filter(models.Data.attributes_rel.any(models.Attribute.name.in_(attributesList))) which returns all rows which has any of the attribute from attributesList .... but I want only get the models.Data rows which has exactly the attributes from the list (or even others, too, but at least the ones from the list)
Optical sample of my issue:
This three attributes are associated to Data rows. I have set attributeList=['Test2','Test3'] ... but also the last row is returned .. because it has attribute Test3 but it should not be returned because it has not Test2 ... any idea?
in_ will basically do any of the attributes is present, whereas what you want is ALL attributes are present.
To achieve this, just add filter for each attribute name separately:
q = session.query(Data)
for attr in attributesList:
q = q.filter(Data.attributes_rel.any(Attribute.name == attr))

Count related items in a sqlalchemy model using ChoiceType

This is a follow up to a previous question here. I'd like to count the number of offers, in each category, and output them in a format, which I can iterate in Jinja.
new, 3
used, 7
broken, 5
Here's what I've got right now:
class Offer(Base):
CATEGORIES = [
(u'new', u'New'),
(u'used', u'Used'),
(u'broken', u'Broken')
]
__tablename__ = 'offers'
id = sa.Column(sa.Integer, primary_key=True)
summary = sa.Column(sa.Unicode(255))
category = sa.Column(ChoiceType(CATEGORIES))
Following the previous answer, I tried something like this:
count_categories = db.session.query(
CATEGORIES.value, func.count(Offer.id)).outerjoin(
Offer).group_by(CATEGORIES.key).all()
This obviously doesn't work because CATEGORIES.value is not defined; How can I pass CATEGORIES to this query, to yield the desired result? The "setup" seems fairly common, and is taken straight from the SQLAlchemy-Utils Data types page
Your help is much appreciated (growing white hair already)!
A horrible but working, temporary work-around:
result = []
for category in Offer.CATEGORIES:
count = db.session.query(func.count(Offer.id)).filter_by(category=category[0]).all()
result.append((category[0], category[1], count[0][0]))
To count the number of offers for each category you need to do an outer join between categories and offers. Given that you are not storing categories as a database table you are having to do this in application code which is not ideal. You just need to create a new categories table and replace the category field in the user table with a foreign key joining to the new categories table. The following code demonstrates this.
class Offer(Base):
__tablename__ = 'offers'
id = sa.Column(sa.Integer, primary_key=True)
summary = sa.Column(sa.Unicode(255))
category_id = sa.Column(sa.Integer, sa.ForeignKey("categories.id"))
category = sa.orm.relationship("Category", back_populates="offers")
class Category(Base):
__tablename__ = 'categories'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Unicode(6), unique=True)
offers = sa.orm.relationship("Offer", back_populates="category")
# populate categories with the same values as your original enumeration
session.add(Category(name="New"))
session.add(Category(name="Used"))
session.add(Category(name="Broken"))
count_categories = session.query(Category.name, func.count(Offer.id)). \
select_from(Category).outerjoin(Offer).group_by(Category.name).all()

SQLAlchemy querying with joins

With this models:
class Ball(Base):
__tablename__ = 'balls'
id = Column(Integer, primary_key=True)
field_id = Column(Integer, ForeignKey('fields.id'))
field = relationship("Field", back_populates="fields")
class Field(Base):
__tablename__ = 'fields'
id = Column(Integer, primary_key=True)
nickname = Column(String)
fields= relationship("Ball", order_by=Ball.id, back_populates="field")
I'm trying to write query to access Field.nickname and Ball.field_id. With use of
result = session.query(Field, Ball).all()
print(result)
I'm able to retrieve
(<Field(id=1, nickname=stadium>, <Ball(id=1, field_id=newest)>), (<Field(id=1, nickname=stadium>, <Ball(id=2, field_id=oldest)>
but when using
result = session.query(Field).join(Ball).all()
print(result)
I'm getting empty list [], doesn't matter if I'm applying any filters or not. According to official docs, join would come in handy if dealing with two tables, so this way I would be able to filter for the same id in both and I guess it would came in handy when displaying query results in jinja template.
You have a typo in your Field class - back_populates='fields' doesn't match up with the attribute name in Ball.
Try changing it to back_populates='field' so that it matches the attribute defined in Ball. With the relationship defined correctly sqlalchemy should be able to do the join automatically.

Tracking member tagging of artists

I'm writing a web based jukebox for my own amusement and as a learning experiencing.
I'm finally getting around to finishing my models except for one big component: member genre tags. I know I'll have to relate three models and it'll involve using association_proxy and collection classes.
Here's the relevant models (I had an abstract model to handle declaring the id and name fields, but that caused issues that I'll look at later):
class Member(db.Model):
__tablename__ = 'members'
id = db.Column('id', db.Integer, primary_key=True)
name = db.Column('name', db.String(128))
##tagged_artists is a backref from MemberTaggedArtist
tags = association_proxy('tagged_artists', 'artist',
creator=lambda k,v: MemberTaggedArtist(tag=k, artist=v)
)
class Artist(db.Model):
__tablename__ = 'artists'
id = db.Column('id', db.Integer, primary_key=True)
name = db.Column('name', db.String(128))
class Tag(db.Model):
__tablename__ = 'tags'
id = db.Column('id', db.Integer, primary_key=True)
name = db.Column('name', db.String(128))
class MemberTaggedArtist(db.Model):
__tablename__ = 'membertaggedartists'
id = db.Column('id', db.Integer, primary_key=True)
member_id = db.Column('member_id', db.Integer, db.ForeignKey('members.id'))
artist_id = db.Column('artist_id', db.Integer, db.ForeignKey('artists.id'))
tag_id = db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
member = db.relationship(Member, backref=db.backref(
'tagged_artists',
collection_class=attribute_mapped_collection('tag')
)
artist = db.relationship(Artist, backref='applied_tags')
tag = db.relationship(Tag, backref='applied_artists')
What I'd like to happen is this:
>>> member = Member(name='justanr')
>>> artist = Artist(name='Gorguts')
>>> tag = Tag('Death Metal')
>>> member.tags['Death Metal'].append('Gorguts')
>>> member.tags
... {'Death Metal':['Gorguts']}
What currently happens is this (note, I built a mixin to handle repr calls):
>>> member.tags
... {Tag (ID:1 Name:Death Metal): MemberTaggedArtist (ID: 1 Member:justanr Artist:Gorguts Tag:Death Metal)}
I haven't been working with association_proxy long enough to understand what I'm doing wrong and even the brief tutorial in the documentation is giving me issues (I'm not sure why and I don't think it's because I'm using Flask-SQLAlchemy).
In short, I'm attempting to build an association proxy to create a dict of lists and I'm completely lost. I'm unsure what values I should proxy along, if using one middle table is over complicating this, and how to construct a secondary (and possibly tertiary) middle table
I finally came up with a solution. I forgot what I knew about SQL...lol I guess? But I recently watched several talks on SQLA that made me realize that I had an 'O' problem -- I was focusing specifically on the Object portion of ORM instead of thinking "SQLA just makes writing Queries easier, it doesn't do it for me." It also didn't help that FSQLA's query object lulled me in complacency either. Don't get me wrong, it's fantastic for basic queries and joins, but for something this complex...not really.
I also didn't realize that my question was actually several questions. One part was "How do I write this query with SQLA's tools?" The other was, "Help me puzzle the correct association_proxy to get this desired outcome."
I'm still not sure about the association proxy portion of it yet, or if I even want to do that anymore. But I finally puzzled the solution to my first question out:
# Query database for tag names and count for a particular artist
artist = models.Artist.find_by_some_means(magic, vars)
q = db.session.query(
models.Tag.id.label('id'),
models.Tag.name.label('tag'),
db.func.count(models.MemberTaggedArtist.member_id).label('count')
)
q = q.join(models.MemberTaggedArtist, models.Tag.id == models.MemberTaggedArtist.tag_id)
q = q.filter(models.MemberTaggedArtist.artist_id == artist.id)
q = q.group_by(models.MemberTaggedArtist.tag_id)
# order by tag count first, then tag name if two tags happen to have the same count
# there's probably a much better way to get there without recalculating the result
q = q.order_by(db.desc(db.func.count(models.MemberTaggedArtist.member_id)))
q = q.order_by(models.Tag.name)
My original planned query was something along the lines of:
SELECT tags.name AS tag, tags.id AS id, count(mta.member_id) AS count
FROM tags, membertaggedartists AS mta
WHERE tags.id = mta.tag_id AND mta.artist_id = :artist_id
GROUP BY id
ORDER BY count DESC, name ASC
And SQLA interprets my request as:
SELECT tags.id AS id, tags.name AS tag, count(membertaggedartists.member_id) AS count
FROM tags
JOIN membertaggedartists ON tags.id = membertaggedartists.tag_id
WHERE membertaggedartists.artist_id = :artist_id_1
GROUP BY membertaggedartists.tag_id
ORDER BY count(membertaggedartists.member_id) DESC, tags.name
Which is remarkably similar (and uses an explicit join unlike mine, which negates the need for one of my original WHERE clauses).

Categories