How do I update related object on update in SQLAlchemy? - python

I have a one-to-one relationship between a Rule and a RuleStats object. I am trying to modify the last_updated field on the RuleStats when any field on the Rule is updated. If they were not two separate classes it would look something like this.
class Rule(Base):
__tablename__ = 'rules'
def __init__(self, **kwargs):
Base.__init__(self, **kwargs)
self.stats = RuleStats(rule=self)
id = Column(Integer, autoincrement=True, primary_key=True)
description = Column(String, nullable=False, default='')
# ... more fields
last_updated = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
But how do I do it if I have two separate objects,
class Rule(Base):
__tablename__ = 'rules'
def __init__(self, **kwargs):
Base.__init__(self, **kwargs)
self.stats = RuleStats(rule=self)
id = Column(Integer, autoincrement=True, primary_key=True)
description = Column(String, nullable=False, default='')
# ... more fields
stats = relationship('RuleStats', uselist=False, backref='rule', cascade='all, delete-orphan')
class RuleStats(Base):
__tablename__ = 'rule_stats'
def __init__(self, **kwargs):
Base.__init__(self, **kwargs)
rule_id = Column(Integer, ForeignKey('rules.id'), primary_key=True)
last_updated = Column(DateTime, default=datetime.now, nullable=False)

I think the simplest approach would be
Make function Rule.update() and do updates only through this specific function in your application. Example:
from sqlalchemy.orm.attributes import set_attribute
class Rule:
def update(self, **kwargs):
"""Update instance attributes and bump last_modified date in stats"""
for key, value in kwargs.items():
set_attribute(self, key value)
self.stats.last_updated = now()
Then:
rule.update(description="Python rocks!")
If you want to make it transparent to the user, so that RuleState is always updated, even though you poke Rule attributes directly, you could use Python properties for this. This approach, however, leads more complex code base and I could not recommend it outhand.
Alternatively, you can use database triggers to execute the update on the database side. Update trigger example for PostgreSQL.
There most likely exists an method doing this internally in advanced SQLAlchemy, but I don't have enough insight to tell where to start poking.

Related

How to add a custom function/method in sqlalchemy model to do CRUD operations?

Below I have a Flask-SQLAlchemy model for the table User.
class User(db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
def __init__(self):
#self.name = name
self.name = None
def add_user(self, name):
self.name = name
Here add_user is a custom method. So if I call the add_user method it should add the name to the User table.
Likewise how do I write custom methods for CRUD operations in that model itself?
You'll probably want to use a classmethod to accomplish this.
class User(db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
def __init__(self, name):
self.name = name
#classmethod
def create(cls, **kw):
obj = cls(**kw)
db.session.add(obj)
db.session.commit()
This way you can use User.create(name="kumaran") to create a new user that will be committed to the database.
Better yet, it is a great idea to create a mixin for this method and others like it so that the functionality can be easily reused in your other models:
class BaseMixin(object):
#classmethod
def create(cls, **kw):
obj = cls(**kw)
db.session.add(obj)
db.session.commit()
You can then reuse this functionality in your models by using multiple inheritance, like so:
class User(BaseMixin, db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
def __init__(self, name):
self.name = name
Not sure this is relevant to Flask-SQLAlchemy, but basic SQLAlchemy has examples of creating Mixin classes or augmenting the Base class.
https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/mixins.html
e.g.
from sqlalchemy.ext.declarative import declared_attr
class MyMixin(object):
#declared_attr
def __tablename__(cls):
return cls.__name__.lower()
__table_args__ = {'mysql_engine': 'InnoDB'}
__mapper_args__= {'always_refresh': True}
id = Column(Integer, primary_key=True)
class MyModel(MyMixin, Base):
name = Column(String(1000))
I would accomplish what you're after like this:
class User(db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP"))
def __init__(self):
#self.name = name
self.name = None
#classmethod
def add_user(cls, session, name):
user = User(name)
session.add(user)
return User
Then in whatever context you're using it in, create a session, call your method, and commit it.
from .user import User
session = Session()
# or if using Flask SQLAlchemy
# session = db.session
User.add_user(session, 'Foo')
session.commit()
From the sqlalchemy docs:
Keep the lifecycle of the session (and usually the transaction) separate and external.
In contrast to one of the other answers, which assumes you are using this model in a Flask app with FlaskSQLAlchemy's global db.session object, parametrizing the session object like this keeps your model code separate from your session management code. This allows it to be used flexibly in many different contexts.

SQLAlchemy Inserting Data in a Many-to-Many Relationship with Association Table

I've seen a few questions similar to this but none quite hit the nail on the head. Essentially I have three table models Center(), Business(), and CenterBusiness() in a Flask Application using SQLAlchemy. Currently I'm adding to said relationship in this manner:
biz = Business(typId=form.type.data, name=form.name.data,
contact=form.contact.data, phone=form.phone.data)
db.session.add(biz)
db.session.commit()
assoc = CenterBusiness(bizId=biz.id, cenId=session['center'])
db.session.add(assoc)
db.session.commit()
As you can see that's a bit ugly and I know there is a way to do it in one hit with the relationship as they are defined. I see on SQLAlchemy's docs they have a explanation of working with such a table but I can't seem to get it to work.
#Directly from SQLAlchemy Docs
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)
#My Version Using my Tables
center = Center.query.get(session['center']
assoc = CenterBusiness()
assoc.business = Business(typId=form.type.data, name=form.name.data,
contact=form.contact.data, phone=form.phone.data)
center.businesses.append(assoc)
db.session.commit()
Unfortunately, that doesn't seem to be doing the trick... Any help would be greatly appreciated and below I've posted the models involved.
class Center(db.Model):
id = db.Column(MEDIUMINT(8, unsigned=True), primary_key=True,
autoincrement=False)
phone = db.Column(VARCHAR(10), nullable=False)
location = db.Column(VARCHAR(255), nullable=False)
businesses = db.relationship('CenterBusiness', lazy='dynamic')
employees = db.relationship('CenterEmployee', lazy='dynamic')
class Business(db.Model):
id = db.Column(MEDIUMINT(8, unsigned=True), primary_key=True,
autoincrement=True)
typId = db.Column(TINYINT(2, unsigned=True),
db.ForeignKey('biz_type.id',
onupdate='RESTRICT',
ondelete='RESTRICT'),
nullable=False)
type = db.relationship('BizType', backref='businesses',
lazy='subquery')
name = db.Column(VARCHAR(255), nullable=False)
contact = db.Column(VARCHAR(255), nullable=False)
phone = db.Column(VARCHAR(10), nullable=False)
documents = db.relationship('Document', backref='business',
lazy='dynamic')
class CenterBusiness(db.Model):
cenId = db.Column(MEDIUMINT(8, unsigned=True),
db.ForeignKey('center.id',
onupdate='RESTRICT',
ondelete='RESTRICT'),
primary_key=True)
bizId = db.Column(MEDIUMINT(8, unsigned=True),
db.ForeignKey('business.id',
onupdate='RESTRICT',
ondelete='RESTRICT'),
primary_key=True)
info = db.relationship('Business', backref='centers',
lazy='joined')
archived = db.Column(TINYINT(1, unsigned=True), nullable=False,
server_default='0')
I was able to get this working, my problem lied in the following bit of code (error in bold):
#My Version Using my Tables
center = Center.query.get(session['center']
assoc = CenterBusiness()
**assoc.info** = Business(typId=form.type.data, name=form.name.data,
contact=form.contact.data, phone=form.phone.data)
center.businesses.append(assoc)
db.session.commit()
As explained in my comment in the question:
Alright my issue was that I was not using the relationship key "info"
I have in my CenterBusiness model to define the appended association.
I was saying center.business thinking that the term business in that
case was arbitrary. However, I needed to actually reference that
relationship. As such, the appropriate key I had setup already in
CenterBusiness was info.
I will still accept any updates and/or better ways to handle this situation, though I think this is the best route at the time.
below example can help u
more details http://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)

Creating new model in sqlalchemy triggers INSERT on database

While writing new test I found quite curious case:
models.ArticleTag(tag=tag, article=article)
Triggers INSERT statement on database even though there is no Session.add after.
However if I change code to look like this:
models.ArticleTag(tag.id=tag.id, article=article)
No INSERT will ever happen.
Article in this case is fresh object that is not in db yet and tag is something that we have already in db.
My models look like this:
class Tag(object):
__tablename__ = 'tag'
title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
type = sqlalchemy.Column(sqlalchemy.Unicode(255))
uid = sqlalchemy.Column(sqlalchemy.Unicode(32), primary_key=True, nullable=False)
__table_args__ = (
sqlalchemy.UniqueConstraint('type', 'title', name='title_type_unique'),
)
class ArticleTag(object):
"""Represent m2m table between Tags and Article.
association obj for mapping m2m articles<->tags
"""
__tablename__ = 'article_tag'
article_uid = sqlalchemy.Column(sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('article.uid'), primary_key=True)
tag_uid = sqlalchemy.Column(sqlalchemy.Unicode(32), sqlalchemy.ForeignKey('tag.uid'), primary_key=True)
priority = sqlalchemy.Column(sqlalchemy.Integer, default=0)
tag = sqlalchemy.orm.relationship('Tag', backref='articles')
class Article(object):
__tablename__ = 'article'
uid = sqlalchemy.Column(sqlalchemy.Unicode(32), primary_key=True, nullable=False)
title = sqlalchemy.Column(sqlalchemy.Unicode(255), nullable=False)
meta_description = sqlalchemy.Column(sqlalchemy.Unicode(255))
meta_title = sqlalchemy.Column(sqlalchemy.Unicode(255))
body = sqlalchemy.Column(sqlalchemy.Unicode)
is_active = sqlalchemy.Column(sqlalchemy.Boolean, default=True)
# any tags we have
article_tags = sqlalchemy.orm.relationship('ArticleTag', backref='article', cascade='all, delete-orphan', order_by=lambda: sqlalchemy.desc(ArticleTag.priority))
#property
def tags(self):
"""Helper method that will return tags sorted by priority.
"""
return [art_tag.tag for art_tag in self.article_tags]
My guess is that I do not know something quite important about sqlalchemy.
Anyone can point me a direction?

Creating hybrid properties automatically from SQLAlchemy Mixins

In SQLAlchemy, I'd like to have a class that automatically creates hybrid properties to expose attributes from a specific child table. Consider this structure:
class Address(Model):
id = Column(Integer, primary_key=True)
street = Column(String)
number = Column(Integer)
valid_from = Column(DateTime)
valid_to = Column(DateTime)
person_id = Column(Integer, ForeignKey('person.id'))
person = relationship('Person', backref=backref('addresses', lazy='dynamic')
class Person(db.Model, HybridPropertyGeneratorMixin):
data_class = Address
id = Column(Integer, primary_key=True)
#property
def current_address(self):
return self.addresses.order_by(desc(Address.valid_from))[0]
#hybrid_property
def city(cls):
return self.current_address.city
#city.expression
def city(cls):
return select([Address.name]). \
where(cls.id==Address.person_id). \
where(Address.valid_to == None).as_scalar()
What I'm trying to do is define a mixin that would automatically look at the attributes of data_class and generate hybrid attributes and expressions from the data_class's attributes. For example, I want to automatically define the hybrid property and expression for city, state, street, etc.
UPDATE
Wasn't clear enough in what I originally wanted to do. See above for an update as to why I want to automatically generate hybrid properties and expressions.
You can override the special method __getattr__ to get the attribute from current_address if it's not an attribute of person.
class Person(db.Model):
# ...
def __getattr__(self, item):
return getattr(self.current_address, item)

Flask-SQLAlchemy Abstract Base Model

In my Flask-SQLAlchemy App I want to add a few fields (created(by|on), changed(by|on)) to every Model/Table
my code right now
from .. import db
class Brand(db.Model):
__tablename__ = 'md_brands'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
def __repr__(self):
return u'<Brand {}>'.format(self.name)
I am not sure if it's better to use Mixins or somehow extend the base db.Model (or if even there is a better way to do this).
What (and why) is the best way to add such fields (created(by|on), changed(by|on)) to all my models?
Using __abstract__.
How do I declare a base model class in Flask-SQLAlchemy?
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
class Base(db.Model):
__abstract__ = True
created_on = db.Column(db.DateTime, default=db.func.now())
updated_on = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
class User(Base):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(255), unique = True)
Both are pretty much the same. Here is a Mixin that I use
class ModelMixin(object):
def __repr__(self):
return unicode(self.__dict__)
#property
def public_view(self):
"""return dict without private fields like password"""
return model_to_dict(self, self.__class__)
and then
class User(db.Model, ModelMixin):
""" attributes with _ are not exposed with public_view """
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)

Categories

Resources