SQLAlchemy set default value of column depending on ID - python

I already know that for general cases: this question is sufficient: SQLAlchemy set default value of one column to that of another column
But when my column depends on the Id column it doesn't work because id is auto generated.
For eg: Using the same example as the other question
class Substance(Base):
__tablename__ = "substances"
id = Column(Integer, primary_key=True)
code = Column(String, unique=True)
name = Column(String, unique=True)
subtance_hash = Column(String, unique=True, default=hashgen)
def hashgen(context):
uid = context.get_current_parameters()['id'] + 100
name = context.get_current_parameters()['name']
code = context.get_current_parameters()['code']
return some_hash_function(uid, name, code)
def some_hash_function(uid, name, code):
# this is a stupid example for completeness
return str(uid) + str(name) + str(code)
The problem here is that uid is None because it will be auto generated while committing the query I think?
So it throws an error:
sqlalchemy.exc.StatementError: (builtins.KeyError) 'id'
Is it even possible to do this? If so how?

You can flush your session to set the id before the commit. You can then calculate your hash. You can also use the event system is SQLAlchemy to listen for after_flush events and calculate the hash there. For example:
class Substance(Base):
__tablename__ = "substances"
id = Column(Integer, primary_key=True)
code = Column(String, unique=True)
name = Column(String, unique=True)
subtance_hash = Column(String, unique=True)
#event.listens_for(sessionOrSessionFactory, 'after_flush')
def hashgen(session, flush_context):
for obj in session:
if isinstance(obj, Substance):
obj.substance_hash = some_hash_function(obj)
def some_hash_function(obj):
# this is a stupid example for completeness
return str(obj.id + 100) + str(obj.name) + str(obj.code)
Another method is to make the hash hybrid_property https://docs.sqlalchemy.org/en/13/orm/extensions/hybrid.html
I haven't played around with hybrid properties much so I will leave it for someone else to contribute an answer for that.

Related

SQLAlchemy unique constrain by field

I have UniqueConstraint on field, but it wont allow me to add multiple entries (two is max!)
from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint
class Cart(SqlAlchemyBase):
__tablename__ = 'cart'
__table_args__ = (UniqueConstraint('is_latest'), {})
sid = Column(Integer, primary_key=True)
is_latest = Column(Boolean, index=True, nullable=False)
name = Column(String)
I would like to support more entries, so that one name can have two variants:
name=foo, is_latest=True
name=foo, is_latest=False
name=bar, is_latest=True
name=bar, is_latest=False
but then reject any subsequent attempt to write name=foo (or bar) and is_latest=True
What you are trying to achieve here is a type 2 slowly changing dimension, this is a topic that has been discussed extensively and I encourage you to look it up.
When I look at your table you seem to use sid as a surrogate key, but I fail to see what is the natural key and what will be updated as time goes.
Anyway, there are several ways to achieve SCD type 2 result without the need to worry about your check, but the the simplest in my mind is to keep on adding records with your natural key and when querying, select only the one with highest surrogate key (autoincrementing integer), no need for current uniqueness here as only the latest value is fetched.
There are examples for versioning rows in SQLAlchemy docs, but since website come and go, I'll put a simplified draft of the above approach here.
class VersionedItem(Versioned, Base):
id = Column(Integer, primary_key=True) # surrogate key
sku = Column(String, index=True) # natural key
price = Column(Integer) # the value that changes with time
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.dirty:
if not (
isinstance(instance, VersionedItem)
and session.is_modified(instance)
and attributes.instance_state(instance).has_identity
):
continue
make_transient(instance) # remove db identity from instance
instance.id = None # remove surrogate key
session.add(instance) # insert instance as new record
Looks like a Partial Unique Index can be used:
class Cart(SqlAlchemyBase):
__tablename__ = 'cart'
id = Column(Integer, primary_key=True)
cart_id = Column(Integer)
is_latest = Column(Boolean, default=False)
name = Column(String)
__table_args__ = (
Index('only_one_latest_cart', name, is_latest,
unique=True,
postgresql_where=(is_latest)),
)
name=foo, is_latest = True
name=foo, is_latest = False
name=bar, is_latest = False
name=bar, is_latest = False
And when adding another name=foo, is_latest = True
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "only_one_latest_cart"
DETAIL: Key (name, is_latest)=(foo, t) already exists.

Accessing relationships within the after_insert and before_insert event hooks when using SQLAlchemy

I have two very related questions about manipulating relationships in SQLAlchemy event hooks. Note I'm using flask-sqlalchemy and essentially the same model as below.
1) edit I'm trying to add an event listener for the Record object so that I do something to the people in a Record instance after it has been created by the Record default initializer. The only problem is target in the do_action body doesn't have the people list appended, whereas the people list is present after the instance is created. What's the reason for this and is there a workaround for do_action?
My model:
record_structure = db.Table(
'record_structure',
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
db.Column('record_id', db.Integer, db.ForeignKey('record.id')))
class Person(db.Model):
__tablename__ = 'person'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
class Record(db.Model):
__tablename__ = 'record'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
people = db.relationship('Person', secondary=record_structure)
def __init__(self, name, people):
self.name = name
self.people = list()
self.people.extend(people)
#db.event.listens_for(Record, 'after_insert')
def do_action(mapper, connection, target):
print(target.name) # output 'my record'
print(len(target.people)) # output: 0 ??
# do something with target.people
My load_db.py
p1 = Person(name='tom')
p2 = Person(name='fred')
db.session.add(p1)
db.session.add(p2)
db.session.commit()
r = Record('my record', [p1, p2])
print(len(r.people)) # output: 2
db.session.add(r)
db.session.commit()
2) If there is a workaround for the 'after_insert' event hook. Is there a workaround if I replace the hook with a 'before_insert' event hook?
So it turns out the code I provided in 1) is completely fine so len(target.people) == 2 and 2) holds as well. My original code was slightly different and that's what caused the len(target.people) == 0. The bookkeeper.records.append(self) seems to have caused a premature insert and caused self.people to initialize as empty list. Thanks #SuperShoot for trying to reproduce it.
class Record(db.Model):
__tablename__ = 'record'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, nullable=False)
people = db.relationship('Person', secondary=record_structure)
def __init__(self, name, bookkeeper, people):
self.name = name
bookkeeper.records.append(self)
self.people = list()
self.people.extend(people)
I guess a good practice is to put lines that pass self like bookkeeper.records.append(self) at the end of __init__ so relevant data is passed to event hooks.

Creating new instances in SQLAlchemy using lookup values instead of foreign key IDs

I have an existing database an want to build an SQLAlchemy wrapper to use the DB in Python. Lookup tables like the following are commonly used in the DB:
class Industry(Base):
__tablename__ = 'industry'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=True)
class IndustrySector(Base):
__tablename__ = 'industry_sector'
id = Column(Integer, primary_key=True)
industry_id = Column(Integer, ForeignKey('industry.id'), nullable=False)
name = Column(String, nullable=True)
What I would like to do is to create a new instance of IndustrySector using the name of the industry rather than the (technical) key of the industry, i.e.,
new_industry_sector = IndustrySector(industry_id = 'Manufacturing', name = 'Textile')
instead of
manu_industry_id = session.query(Industry.id).filter(Industry.name=='Manufacturing').first().id
new_industry_sector = IndustrySector(name = 'Textile', industry_id = new_industry_id)
Obviously, above example can't work because I am filtering on the ID rather than the name. But I don't know how to get the name of the foreign-keyed table into this. Of course I could simply add a #classmethod that handles the lookup, but if there exists any built-in functionality I'd much rather use that.
Any help / pointers are appreciated
You could create a constructor which will replace the string with the id value whenever an object is created.
class IndustrySector(Base):
__tablename__ = 'industry_sector'
id = Column(Integer, primary_key=True)
industry_id = Column(Integer, ForeignKey('industry.id'), nullable=False)
name = Column(String, nullable=True)
def __init__(self, industry_id, name):
self.name = name
self.industry_id = fetch_id(industry_id)
def fetch_id(industry_id):
# fetch and return the id

How can I use autoincrementing column value as default value for another not nullable column?

Lets say that we have UserModel with following columns:
class UserModel(declarative_base(bind=engine)):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname = Column(String, unique=True, nullable=False)
Is there a way to set id as default value for nickname without changing anything outside the model class?
I tried using default=id and server_default=id, but IntegrityError is always raised on commit.
Also, I know that there is no id until commit or flush is performed, but calling flush outside is not an option for me.
Thanks.
You should use hybrid_property for this. Here example:
class UserModel(Base):
__tablename__ = "users"
id = Column(Integer, autoincrement=True, primary_key=True)
nickname_str = Column(String, unique=True)
#hybrid_property
def nickname(self):
return self.nickname_str or str(self.id)
#nickname.expression
def nickname(self):
return case(
[(self.nickname_str != None, self.nickname_str)],
else_=cast(self.id, String)
).label('nickname')
#nickname.setter
def nickname(self, value):
self.nickname_str = value
Full example here.

With SQLAlchemy, how do I make a dynamic relation?

I have a SyncEntities class (shown below).
I have several other classes (such as CommodityTypes also shown below) related to the SyncEntities class.
All of my Base subclasses have this column uuidKey = Column(String, primary_key=True)
Assume se is an instance of SyncEntities.
se.entityKind is the name of a Base subclass.
How do I query for an object that is in the se.entityKind class filtering for se.uuidKey?
class SyncEntities(Base):
__tablename__ = 'SyncEntities'
uuidKey = Column(String, primary_key=True)
dateCreated = Column(DateTime, index=True)
dateModified = Column(DateTime, index=True)
dateSynced = Column(DateTime, index=True)
username = Column(String)
entityKind = Column(String)
deleted = Column(Boolean)
def __init__(self, entity, security):
self.uuidKey = newUUID()
self.dateCreated = security.now
self.dateModified = security.now
self.dateSynced = security.then
self.username = security.username
self.entityKind = entity.__tablename__
self.deleted = False
def modified(self, security):
self.dateModified = security.now
self.username = security.username
class CommodityTypes(Base):
__tablename__ = 'CommodityTypes'
uuidKey = Column(String, ForeignKey('SyncEntities.uuidKey'), primary_key=True)
myName = Column(String, unique = True)
sortKey = Column(Integer, unique = True)
mySyncEntity = relationship("SyncEntities")
def __init__(self, security, myName, sortKey):
self.syncEntity = SyncEntities(self, security)
self.uuidKey = self.syncEntity.uuidKey
self.myName = myName
self.sortKey = sortKey
The structure here is similar, though not quite the same, as a "polymorphic association", and you can read about this pattern over at this blog post: http://techspot.zzzeek.org/2007/05/29/polymorphic-associations-with-sqlalchemy/ . It's an old post but the example at http://techspot.zzzeek.org/files/2007/discriminator_on_association.py was added later as an updated example.
This case is a little different in that an object like CommodityTypes only refers to a single SyncEntities, not multiple as in the usual polymorphic association. The SyncEntities also can only refer to a single type of related object since you have entityKind on it locally.
I would note that a potential problem with this design is that you could have rows in other tables that have a uuidKey pointing to a particular SyncEntities instance, but are not of a type that matches "entityKind". If the relationship between CommodityTypes and SyncEntities is actually one-to-one, that changes everything - this pattern is really simple joined table inheritance and you'd use the patterns described at http://docs.sqlalchemy.org/en/rel_0_7/orm/inheritance.html.
You also don't have backrefs between the target and SyncEntities, which is often a way to automate these styles of lookup. But you can still approximate things using a lookup of entityKind types to classes:
def lookup_related(se):
types = {
'commodity':CommodityTypes,
'foobar':FooBarTypes
}
cls = types[se.entityKind]
session = object_session(se)
return session.query(cls).filter(cls.mySyncEntity==se).all()
here's a mixin that could do it also, using a backref:
class HasSyncEntity(object):
entity_kind = None
"subclasses need to populate this"
#declared_attr
def uuidKey(cls):
return Column(String, ForeignKey("SyncEntities.uuidKey"), primary_key=True)
#declared_attr
def mySyncEntity(cls):
return relationship("SyncEntities", backref="_%s_collection" % cls.entity_kind)
CommodityTypes becomes:
class CommodityTypes(HasSyncEntity, Base):
entity_kind = "commodity"
# ...
You then add a method like this to SyncEntities, which looks up the appropriate backref, and you're done:
def get_related(self):
return getattr(self, "_%s_collection" % self.entityKind)

Categories