Delete children after parent is deleted in SQLAlchemy - python

My problem is the following:
I have the two models Entry and Tag linked by a many-to-many relationship in SQLAlchemy. Now I want to delete every Tag that doesn't have any corresponding Entry after an Entry is deleted.
Example to illustrate what I want:
Entry 1 with tags python, java
Entry 2 with tags python, c++
With these two entries the database contains the tags python, java, and c++. If I now delete Entry 2 I want SQLAlchemy to automatically delete the c++ tag from the database. Is it possible to define this behavior in the Entry model itself or is there an even more elegant way?
Thanks.

this question was asked awhile back here: Setting delete-orphan on SQLAlchemy relationship causes AssertionError: This AttributeImpl is not configured to track parents
This is the "many-to-many orphan" problem. jadkik94 is close in that you should use events to catch this, but I try to recommend against using the Session inside of mapper events, though it works in this case.
Below, I take the answer verbatim from the other SO question, and replace the word "Role" with "Entry":
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import event
from sqlalchemy.orm import attributes
Base= declarative_base()
tagging = Table('tagging',Base.metadata,
Column('tag_id', Integer, ForeignKey('tag.id', ondelete='cascade'), primary_key=True),
Column('entry_id', Integer, ForeignKey('entry.id', ondelete='cascade'), primary_key=True)
)
class Tag(Base):
__tablename__ = 'tag'
id = Column(Integer, primary_key=True)
name = Column(String(100), unique=True, nullable=False)
def __init__(self, name=None):
self.name = name
class Entry(Base):
__tablename__ = 'entry'
id = Column(Integer, primary_key=True)
tag_names = association_proxy('tags', 'name')
tags = relationship('Tag',
secondary=tagging,
backref='entries')
#event.listens_for(Session, 'after_flush')
def delete_tag_orphans(session, ctx):
# optional: look through Session state to see if we want
# to emit a DELETE for orphan Tags
flag = False
for instance in session.dirty:
if isinstance(instance, Entry) and \
attributes.get_history(instance, 'tags').deleted:
flag = True
break
for instance in session.deleted:
if isinstance(instance, Entry):
flag = True
break
# emit a DELETE for all orphan Tags. This is safe to emit
# regardless of "flag", if a less verbose approach is
# desired.
if flag:
session.query(Tag).\
filter(~Tag.entries.any()).\
delete(synchronize_session=False)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
r1 = Entry()
r2 = Entry()
r3 = Entry()
t1, t2, t3, t4 = Tag("t1"), Tag("t2"), Tag("t3"), Tag("t4")
r1.tags.extend([t1, t2])
r2.tags.extend([t2, t3])
r3.tags.extend([t4])
s.add_all([r1, r2, r3])
assert s.query(Tag).count() == 4
r2.tags.remove(t2)
assert s.query(Tag).count() == 4
r1.tags.remove(t2)
assert s.query(Tag).count() == 3
r1.tags.remove(t1)
assert s.query(Tag).count() == 2
two almost identical SO questions qualifies this as something to have on hand so I've added it to the wiki at http://www.sqlalchemy.org/trac/wiki/UsageRecipes/ManyToManyOrphan.

I will let code speak for me:
from sqlalchemy import create_engine, exc, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import func, Table, Column, Integer, String, Float, Boolean, MetaData, ForeignKey
from sqlalchemy.orm import relationship, backref
# Connection
engine = create_engine('sqlite:///', echo=True)
Base = declarative_base(bind=engine)
Session = sessionmaker(bind=engine)
# Models
entry_tag_link = Table('entry_tag', Base.metadata,
Column('entry_id', Integer, ForeignKey('entries.id')),
Column('tag_id', Integer, ForeignKey('tags.id'))
)
class Entry(Base):
__tablename__ = 'entries'
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False, default='')
tags = relationship("Tag", secondary=entry_tag_link, backref="entries")
def __repr__(self):
return '<Entry %s>' % (self.name,)
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
def __repr__(self):
return '<Tag %s>' % (self.name,)
# Delete listener
def delete_listener(mapper, connection, target):
print "---- DELETING %s ----" % (target,)
print '-' * 20
for t in target.tags:
if len(t.entries) == 0:
print ' ' * 5, t, 'is to be deleted'
session.delete(t)
print '-' * 20
event.listen(Entry, 'before_delete', delete_listener)
# Utility functions
def dump(session):
entries = session.query(Entry).all()
tags = session.query(Tag).all()
print '*' * 20
print 'Entries', entries
print 'Tags', tags
print '*' * 20
Base.metadata.create_all()
session = Session()
t1, t2, t3 = Tag(name='python'), Tag(name='java'), Tag(name='c++')
e1, e2 = Entry(name='Entry 1', tags=[t1, t2]), Entry(name='Entry 2', tags=[t1, t3])
session.add_all([e1,e2])
session.commit()
dump(session)
raw_input("---- Press return to delete the second entry and see the result ----")
session.delete(e2)
session.commit()
dump(session)
This code above uses the after_delete event of the SQLAlchemy ORM events. This line does the magic:
event.listen(Entry, 'before_delete', delete_listener)
This says to listen to all deletes to an Entry item, and call our listener which will do what we want. However, the docs do not recommend changing the session inside the events (see the warning in the link I added). But as far as I can see, it works, so it's up to you to see if this works for you.

Related

How to test if a class object was created using Pytest

I wrote a habit tracker app and used SQLAlchemy to store the data in an SQLite3 database. Now I'm writing the unit tests using Pytest for all the functions I wrote. Besides functions returning values, there are functions that create entries in the database by creating objects. Here's my object-relational mapper setup and the two main classes:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Date
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Setting up SQLAlchemy to connect to the local SQLite3 database
Base = declarative_base()
engine = create_engine('sqlite:///:main:', echo=True)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
session = Session()
class Habit(Base):
__tablename__ = 'habit'
habit_id = Column('habit_id', Integer, primary_key=True)
name = Column('name', String, unique=True)
periodicity = Column('periodicity', String)
start_date = Column('start_date', Date)
class HabitEvent(Base):
__tablename__ = 'habit_event'
event_id = Column('event_id', Integer, primary_key=True)
date = Column('date', Date)
habit_id = Column('fk_habit_id', Integer, ForeignKey(Habit.habit_id))
One of the creating functions is the following:
def add_habit(name, periodicity):
if str(periodicity) not in ['d', 'w']:
print('Wrong periodicity. \nUse d for daily or w for weekly.')
else:
h = Habit()
h.name = str(name)
if str(periodicity) == 'd':
h.periodicity = 'Daily'
if str(periodicity) == 'w':
h.periodicity = 'Weekly'
h.start_date = datetime.date.today()
session.add(h)
session.commit()
print('Habit added.')
Here's my question: Since this functions doesn't return a value which can be matched with an expected result, I don't know how to test if the object was created. The same problem occurs to me, when I want to check if all objects were deleted using the following function:
def delete_habit(habitID):
id_list = []
id_query = session.query(Habit).all()
for i in id_query:
id_list.append(i.habit_id)
if habitID in id_list:
delete_id = int(habitID)
session.query(HabitEvent).filter(
HabitEvent.habit_id == delete_id).delete()
session.query(Habit).filter(Habit.habit_id == delete_id).delete()
session.commit()
print('Habit deleted.')
else:
print('Non existing Habit ID.')
If I understand correctly, you can utilize the get_habits function as part of the test for add_habit.
def test_add_habit():
name = 'test_add_habit'
periodicity = 'd'
add_habit(name, periodicity)
# not sure of the input or output from get_habits, but possibly:
results = get_habits(name)
assert name in results['name']

How to have common module for select() in python sqlalchemy?

I have 3 model classes from SQLAlchemy for my tables Table1 Table2 Table3
'''
from sqlalchemy import create_engine , text , select, MetaData, Table ,func , Column , String , Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import sqlalchemy
from settings import DATABASE_URI
engine=create_engine(DATABASE_URI)
Base = declarative_base()
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
metadata = MetaData(bind=None)
session = Session()
class Table1(Base):
__tablename__ = 'table1'
id = Column(u'id', Integer(), primary_key=True)
name1 = Column(u'name1', String(50))
class Table2(Base):
__tablename__ = 'table2'
id = Column(u'id', Integer(), primary_key=True)
name2 = Column(u'name2', String(50))
class Table3(Base):
__tablename__ = 'table3'
id = Column(u'id', Integer(), primary_key=True)
name3 = Column(u'name3', String(50))
class connectionTest():
def wrapper_connection(self,table,column,value):
#SELECT column FROM table1 WHERE column = value
query = session.query(table)
q = query.filter_by(column = value)
session.execute(q)
def main():
ct = connectionTest()
t1 = Table1()
t2 = Table2()
t3 = Table3()
ct.wrapper_connection(t1,t1.name1, "Table1_Value_Information")
ct.wrapper_connection(t2,t2.name2, "Table2_Value_Information")
ct.wrapper_connection(t3,t3.name3, "Table3_Value_Information")
if __name__ == '__main__':
main()
'''
I want the wrapper connection to handle all the 3 different tables with different columns. Basically to make this as generalized as possible to handle any condition related to (#SELECT column FROM table1 WHERE column = value) Clause through SQLAlchemy ORM or Core library.
The issue I am facing is in this line.
'q = query.filter_by(column = value)'
where I am trying to pass the column information from the function attribute t1.name1
ct.wrapper_connection(t1,t1.name1, "Table1_Value_Information")
Error I am facing:
Traceback (most recent call last):
File "C:\Users<username>\AppData\Local\Programs\Python\Python37\lib\site-packages\sqlalchemy\orm\base.py", line 406, in _entity_descriptor
return getattr(entity, key)
AttributeError: type object 'Table1' has no attribute 'column'
The code in the question needs three changes:
In the wrapper_connection method, use Query.filter instead of Query.filter_by because it will accept a column object directly
Don't call Base.metadata.create_all() until after the model classes have been declared
t1 = Table1() creates a new instance of the Table1 class - a row. You want to query against the table, so use the model classes directly instead.
class connectionTest:
def wrapper_connection(self, table, column, value):
# SELECT column FROM table1 WHERE column = value
query = session.query(table)
# We have the column object, so use filter
# instead of filter_by
q = query.filter(column == value)
session.execute(q)
# Create the tables after the model classes have been declared.
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
def main():
ct = connectionTest()
# Use the model classes directly.
ct.wrapper_connection(Table1, Table1.name1, "Table1_Value_Information")
ct.wrapper_connection(Table2, Table2.name2, "Table2_Value_Information")
ct.wrapper_connection(Table3, Table3.name3, "Table3_Value_Information")

SQLAlchemy cascading polymorphic column updates

I have a parent Employee table and a child Engineer table. From a client perspective I only want to interact with the Employee model. This is easily implemented for READ and DELETE, but issues arise when trying to UPDATE or INSERT.
The sqlalchemy docs state:
Warning
Currently, only one discriminator column may be set, typically on the base-most class in the hierarchy. “Cascading” polymorphic columns are not yet supported.
So it would seem that by default this is not going to work. I'm looking for ideas on how to make this work.
Here's a complete test setup using postgres with psycopg2. The SQL might work with other SQL databases, but I have test any others.
SQL script to create test database (testdb) and tables (employee, engineer):
CREATE DATABASE testdb;
\c testdb;
CREATE TABLE employee(
id INT PRIMARY KEY NOT NULL,
name TEXT,
type TEXT
);
CREATE TABLE engineer(
id INT PRIMARY KEY NOT NULL,
engineer_name TEXT,
employee_id INT REFERENCES employee(id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
Python test script:
As-is the INSERT test will fail, but the DELETE will pass. If you change the code (comment/uncomment) to use the child Engineer model it will pass both cases.
import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
Column,
ForeignKey,
Integer,
Text,
)
Base = declarative_base()
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(Text(), default='John')
type = Column(Text, default='engineer')
__mapper_args__ = {
'polymorphic_identity':'employee',
'polymorphic_on':type,
'with_polymorphic': '*',
}
class Engineer(Employee):
__tablename__ = 'engineer'
id = Column(Integer, ForeignKey('employee.id',
ondelete='CASCADE', onupdate='CASCADE'), primary_key=True)
engineer_name = Column(Text(), default='Eugine')
__mapper_args__ = {
'polymorphic_identity':'engineer',
}
def count(session, Model):
query = session.query(Model)
count = len(query.all())
return count
url = 'postgresql+psycopg2://postgres#localhost/testdb'
engine = sa.create_engine(url)
Base.metadata.bind = engine
Base.metadata.create_all()
Session = orm.sessionmaker(engine)
session = Session()
if __name__ == '__main__':
id=0
print '#'*30, 'INSERT', '#'*30
id += id
# I only want to interact with the Employee table
e = Employee(id=id)
# Use the child model to see the INSERT test pass
# e = Engineer(id=id)
session.add(e)
session.commit()
print 'pass' if count(session, Employee) == count(session, Engineer) else 'fail'
print '#'*30, 'DELETE', '#'*30
# e = session.query(Employee).first()
session.delete(e);
session.commit();
print 'pass' if count(session, Employee) == count(session, Engineer) else 'fail'
session.flush()
Any ideas on how to accomplish this through the sqlalchemy model definitions without having to use explicit controller code?
Thanks!
Edit
Well I'm not getting any love for this one. Anybody have ideas on how to accomplish this with controller code?
Using controller logic this can be accomplished by getting the polymorphic subclass using the polymorphic identity.
I'm adding two functions to encapsulate some basic logic.
def get_polymorphic_class(klass, data):
column = klass.__mapper__.polymorphic_on
if column is None:
# The column is not polymorphic so the Class can be returned as-is
return klass
identity = data.get(column.name)
if not identity:
raise ValueError('Missing value for "' + column.name + '"', data)
mapper = klass.__mapper__.polymorphic_map.get(identity)
if mapper:
return mapper.class_
else:
raise ValueError('Missing polymorphic_identity definition for "' + identity + '"')
return klass
def insert(klass, data):
klass = get_polymorphic_class(klass, data)
e = klass(**data)
session.add(e)
session.commit()
return e
Now I update main to use the insert function and everything works as expected:
if __name__ == '__main__':
id=0
print '#'*30, 'INSERT', '#'*30
id += id
e = insert(Employee, {'id': id, 'type': 'engineer'})
print 'pass' if count(session, Employee) == count(session, Engineer) else 'fail'
print '#'*30, 'DELETE', '#'*30
session.delete(e);
session.commit();
print 'pass' if count(session, Employee) == count(session, Engineer) else 'fail'
session.flush()
There's some extra code in my encapsulation for reusability, but the important part is doing Employee.__mapper__.polymorphic_map['engineer'].class_ which returns the Engineer class so we can do a proper cascading INSERT.

Sqlalchemy: Mapping a class to different tables depending on attribute

I'm trying to write to an existing database consisting of multiple tables of the following form
total_usage_<application>:
id
version
date
where <application> runs over a number of strings, say "appl1", "appl2" etc. Now I would like to use SQLAlchemy to create a single class like
class DBEntry:
id = ''
application = ''
version = ''
date = ''
such that an instance foo of DBEntry gets mapped to the table "total_usage_" + foo.application. How can this be achieved?
OK, please take a look at example below, which is self-contained and might show you one way this can be done. It assumes that you know the app_name when you start your program, as well as it assumes that table names follow some naming convention, which obviously you can adapt to your needs or have it completely manually configured by overriding __tablename__ in each mapped class.
But the main idea is that the configuration is wrapped into a function (which can be done also using module import with pre-defined constant beforehand).
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
def camel_to_under(name):
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def configure_database(app_name):
""" #return: dictionary with all the classes mapped to proper tables for
specific application. """
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import declarative_base
class Base(object):
# docs: http://docs.sqlalchemy.org/en/rel_0_9/orm/extensions/declarative.html#augmenting-the-base
#declared_attr
def __tablename__(cls):
return camel_to_under(cls.__name__) + "_" + app_name
def __repr__(self):
attrs = class_mapper(self.__class__).column_attrs # only columns
# attrs = class_mapper(self.__class__).attrs # show also relationships
return u"{}({})".format(self.__class__.__name__,
', '.join('%s=%r' % (k.key, getattr(self, k.key))
for k in sorted(attrs)
)
)
Base = declarative_base(cls=Base)
class Class1(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
class Class1Child(Base):
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey(Class1.id))
name = Column(String)
# relationships
parent = relationship(Class1, backref="children")
# return locals()
return {
"app_name": app_name,
"Base": Base,
"Class1": Class1,
"Class1Child": Class1Child,
}
def _test():
""" Little test for the app. """
engine = create_engine(u'sqlite:///:memory:', echo=True)
session = scoped_session(sessionmaker(bind=engine))
app_name = "app1"
x = configure_database(app_name)
# assign real names
app_name = x["app_name"]
Base = x["Base"]
Class1 = x["Class1"]
Class1Child = x["Class1Child"]
# create DB model (not required in production)
Base.metadata.create_all(engine)
# test data
cc = Class1Child(name="child-11")
c1 = Class1(name="some instance", children=[cc])
session.add(c1)
session.commit()
_test()

mysql Compress() with sqlalchemy

table:
id(integer primary key)
data(blob)
I use mysql and sqlalchemy.
To insert data I use:
o = Demo()
o.data = mydata
session.add(o)
session.commit()
I would like to insert to table like that:
INSERT INTO table(data) VALUES(COMPRESS(mydata))
How can I do this using sqlalchemy?
you can assign a SQL function to the attribute:
from sqlalchemy import func
object.data = func.compress(mydata)
session.add(object)
session.commit()
Here's an example using a more DB-agnostic lower() function:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base= declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
data = Column(String)
e = create_engine('sqlite://', echo=True)
Base.metadata.create_all(e)
s = Session(e)
a1 = A()
a1.data = func.lower("SomeData")
s.add(a1)
s.commit()
assert a1.data == "somedata"
you can make it automatic with #validates:
from sqlalchemy.orm import validates
class MyClass(Base):
# ...
data = Column(BLOB)
#validates("data")
def _set_data(self, key, value):
return func.compress(value)
if you want it readable in python before the flush, you'd need to memoize it locally and use a descriptor to access it.

Categories