Computing field values on save? - python

class Quote(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text)
votes = db.Column(db.Integer)
author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
date_added = db.Column(db.DateTime,default=datetime.datetime.now())
last_letter = db.Column(db.String(1))
I have a Model that looks like the above. I want last_letter to be the last letter of whatever the content is. Where should I place this logic so that it will occur every time a model is saved? I'm reading about Hybrid Properties and stuff and I'm not sure which way is the correct one to go.

1.the Naive way: you can use sqlalchemy column default value to set something like:
last_letter = db.Column(db.char, default=content[len(content)-1:])
didn't check if that would actually work, guess not.
2.you can also do something like adding this init to the class:
def __init__(self,id,content,votes,auther_id,date_added):
self.id = id
self.content = content
#yadda yadda etc
self.last_letter = content[len(content)-1:] #or something similiar
or you could use "listen" to the "before insert" event and add this dynamically as explained here.
you can use sql computed column with an sql trigger (in the db) without sqlalchemy.
you can probably use a sqlalchemy mapper sql expression as a hybrid property, also I didn't try that myself, look simple enough and probably is the most elegant way to do this.

last_letter could be decorated with #property and defined
#property
def last_letter(self):
return self.content[-1]
Disclaimer: I just learned how to use decorators and am using them everywhere

Related

SQLAlchemy hybrid_property order_by using text

Given the following Model
class Person(db.Model):
__tablename__ = 'persons'
name = db.Column(db.String(10), nullable=False)
age = db.Column(db.Integer(), nullable=False)
def __init__(self):
self.name = ''
self.age = 0
#hybrid_property
def is_configured(self):
return self.name != '' and self.age > 0
Is it possible to construct a query using order_by on is_configured hybrid_property using sqlalchemy.text?
If we use the ORM to order_by it works
result = Person.query.filter(or_(*some_filters)).order_by(Person.is_configured).all()
Using sqlalchemy.text results in SQL error stating no column persons.is_configured
result = Person.query.filter(or_(*some_filters)).order_by(text('persons.is_configured asc')).all()
UPDATE 2021-07-16
Some background on why this question was opened: We have a template that renders a table for user accounts where some of the columns are fields on related tables. Clicking the header of a column will sort by the column, sending request to the server with table_name.column to order_by. We have one case where the column is a property. We'd like to make this a hybrid_property so we can query and order by it. We could make this work by mapping the text to the ORM, but if there is a way to make it work with the text that the view provides that would be preferred.
No, this is not possible. The TextClause is pure SQL and executed against the database. As the hybrid property is an ORM object the database doesn’t know anything about this.
P.s.: There are a few ways to achieve what you want but maybe elaborate a bit more about what you are trying to achieve

Generic query method based of an object from its model class

In my model class, I want to create a generic method say get_list(obj) which accept an argument of its object which contains values of their corresponding attribute, and returns all appropriate records that match with corresponding column.
Suppose that I have a users class in my model, and to use the get_list(obj) method. I just need to pass an object of users with its values. Obviously, this will save a lot of time instead of creating repetitive filter_by().
class Users(db.Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, nullable=False, autoincrement=True)
username = Column(String(200), nullable=False)
password = Column(String(200))
email = Column(String(200), nullable=False, unique=True)
def as_query(self):
query = []
for c in self.__table__.columns:
if getattr(self, c.name) is not None:
query.append(c.name+'='+str(getattr(self, c.name)))
return ' and '.join(query)
#classmethod
def get_list(cls, statement):
return cls.query.filter_by(statement).all()
To use the method, we can expect something more like this
user = Users(username='admin')
results = Users.get_list(user.as_query()) # result as a list
I'm aware that we can achieve the same thing with just write the query into filter_by instead of creating a meaningless object. However, in one of my APIs the object will be created automatically-meaning it will be automatic on the fly.
However, this solution is only a hack to just flatten the attribute and its value into filter_by() statement and obviously not working?
Do you have a better solution for this?
Not sure, If this is what you were looking for, but I believe with Python's dict unpacking, it is already possible to unpack all the object attributes to use as filter in the filter_by function.
results = session.query(models.Users).filter_by(**user.dict()).all()
You can also use the or_ method to match any of the attribute values like below:
session.query(Users).filter(or_(**user.dict()))

How do I set the attributes of a SQLAlchemy ORM class before query?

For example, using Flask-SQLAlchemy and jsontools to serialize to JSON like shown -here-, and given a model like this:
class Engine(db.Model):
__tablename__ = "engines"
id = db.Column(db.Integer, primary_key=True)
this = db.Column(db.String(10))
that = db.Column(db.String(10))
parts = db.relationship("Part")
schema = ["id"
, "this"
, "that"
, "parts"
]
def __json__(self):
return self.schema
class Part(db.Model):
__tablename__ = "parts"
id = db.Column(db.Integer, primary_key=True)
engine_id = db.Column(db.Integer, db.ForeignKey("engines.id"))
code = db.Column(db.String(10))
def __json__(self):
return ["id", "code"]
How do I change the schema attribute before query so that it takes effect on the return data?
enginelist = db.session.query(Engine).all()
return enginelist
So far, I have succeeded with subclassing and single-table inheritance like so:
class Engine_smallschema(Engine):
__mapper_args__ = {'polymorphic_identity': 'smallschema'}
schema = ["id"
, "this"
, "that"
]
and
enginelist = db.session.query(Engine_smallschema).all()
return enginelist
...but it seems there should be a better way without needing to subclass (I'm not sure if this is wise). I've tried various things such as setting an attribute or calling a method to set an internal variable. Problem is, when trying such things, the query doesn't like the instance object given it and I don't know SQLAlchemy well enough yet to know if queries can be executed on pre-made instances of these classes.
I can also loop through the returned objects, setting a new schema, and get the wanted JSON, but this isn't a solution for me because it launches new queries (I usually request the small dataset first).
Any other ideas?
The JSON serialization takes place in flask, not in SQLAlchemy. Thus, the __json__ function is not consulted until after you return from your view function. This has therefore nothing to do with SQLAlchemy, and instead it has to do with the custom encoding function, which presumably you can change.
I would actually suggest not attempting to do it this way if you have different sets of attributes you want to serialize for a model. Setting a magic attribute on an instance that affects how it's serialized violates the principle of least surprise. Instead, you can, for example, make a Serializer class that you can initialize with the list of fields you want to be serialized, then pass your Engine to it to produce a dict that can be readily converted to JSON.
If you insist on doing it your way, you can probably just do this:
for e in enginelist:
e.__json__ = lambda: ["id", "this", "that"]
Of course, you can change __json__ to be a property instead if you want to avoid the lambda.

Sqlalchemy - update column based on changes in another column

I'm using sqlalchemy but find documentation difficult to search.
I've these two columns:
verified = Column(Boolean, default=False)
verified_at = Column(DateTime, nullable=True)
I'd like to create a function that does something like this:
if self.verified and not oldobj.verified:
self.verified_at = datetime.datetime.utcnow
if not self.verified and oldobj.verified:
self.verified_at = None
I'm not sure where to put code like this. I could put it in the application, but would prefer the model object took care of this logic.
I think what you're looking for is a Hybrid Property.
from sqlalchemy.ext.hybrid import hybrid_property
class VerifiedAsset(Base):
id = Column(Integer, primary_key=True)
verified_at = Column('verified_at', String(24))
#hybrid_property
def verification(self):
return self.verified_at;
#verification.setter
def verification(self, value):
if value and not self.verification:
self.verified_at = datetime.datetime.utcnow
if not value and self.verification:
self.verified_at = None
# Presumably you want to handle your other cases here
You want to update your verified_at value in a particular way based on some incoming new value. Use properties to wrap the underlying value, and only update when it is appropriate, and only to what you're actually persisting in the db.
You can use sqlalchemy's events registration to put code like that: http://docs.sqlalchemy.org/en/latest/core/event.html.
Basically, you can subscribe to certain events that happen in the Core and ORM. I think it's a clean way to manage what you want to achieve.
You would use the listen_for() decorator, in order to hook when those columns change.
Reading "Changing Attribute Behavior" and "ORM Events" is a good start on trying to solve this type of problem.
One way to go about it would be to set an event listener that updates the timestamp:
#event.listens_for(MyModel.verified, 'set')
def mymodel_verified_set(target, value, oldvalue, initiator):
"""Set verified_at"""
if value != oldvalue:
target.verified_at = datetime.datetime.utcnow() if value else None

How do I create a one-to-many relationship with a default one-to-one property for I18N

Suppose we have these classes:
class Item(Base):
id = Column(Integer, primary_key=True)
data = Column(String)
i18ns = relationship("ItemI18n", backref="item")
class ItemI18n(Base):
lang_short = Column(String, primary_key=True)
item_id = Column(Integer, ForeignKey('item.id'), primary_key=True)
name = Column(String)
The idea here is to have this item's name in multiple languages, for example in English and German. This works fine so far, one can easily work with that. However, most times, I am not interested in all (i.e. both) names but only the users locale.
For example, if the user is English and wants to have the name in his language, I see two options:
# Use it as a list
item = session.query(Item).first()
print item.data, [i18n.name for i18n in item.i18ns if i18n.lang_short == "en"][0]
# Get the name separately
item, name = session.query(Item, ItemI18N.name).join(ItemI18N).filter(ItemI18N.lang_short == "en").first()
print item.data, name
The first one filters the list, the second one queries the language separately. The second is the more efficient way as it only pulls the data really needed. However, there is a drawback: I now have to carry around two variables: item and name. If I were to extend my ItemI18N for example, add a description property, then I would query for ItemI18N and carry those around.
But business logic is different: I would expect to have an Item with a name and description attribute, so that I would do something like this:
item = session.query(Item).first()
print item.data, item.name
So that's where I want to go: Pull all those attributes from Item18N directly into Item. And of course, I would have to specify the language anywhere. However, I cannot find any recipes for this since I don't even know what to search for. Can SQLAlchemy do such a thing?
I also created a complete example for everything I described (except of course the part I don't know how to realize).
Edit: I have played around a bit more to see whether I can come up with a better solution and so far, I have found one way that works. I initially tried to realize it with Query.get but this doesn't work beyond my simple example, because reality is different. To explain, I have to extend my initial model by adding a Language table and turn ItemI18N into a many-to-many relationship with the primary key being (lang_id, item_id):
class ItemI18N(Base):
lang_id = Column(Integer, ForeignKey('language.id'), primary_key=True)
item_id = Column(Integer, ForeignKey('item.id'), primary_key=True)
name = Column(String)
language = relationship("Language", lazy="joined")
class Language(Base):
id = Column(Integer, primary_key=True)
short = Column(String)
Now to get my correct locale I simply turn all loadings into joined loadings by applying lazy="joined" to the complete path. This will inevitable pull in all languages thus returning more data than I need. My approach is then completely independent of SQLAlchemy:
class Item(Base):
...
i18ns = relationship("ItemI18N", backref="item", cascade="all, delete-orphan", lazy="joined")
def name(self, locale):
for i18n in self.i18ns:
if i18n.language.short == locale:
return i18n.name
But this is not a pretty solution, both because of the overhead of retrieving all I18N data from the database and then fitering that result back to one thus making it completely irrelevant that I pulled all in the first place (since the locale will stay the same the whole time). My new full example shows how only one query is executed and gives my transparent access - but with an ugly overhead I would like to avoid.
The example also contains some playing around with transformations I have done. This could point to a solution from that direction, but I wasn't happy with this either because it required me to pass in the with_transformation part every time. I'd like it much better if this would automatically be applied when Item is queried. But I have found no event or other for this.
So now I have multiple solution attempts that all lack the ease of direct access compared to the business logic described above. I hope someone is able to figure out how to close these gaps to produce something nice and clean.

Categories

Resources