SQLAlchemy lazy sa.select([]) - sqlalchemy

I use column properties to refer to properties of other tables.
I'd like to be able to select arbitrary information from other models indepentend of the declaration order of the models.
Just like sa.orm.relationship declares its relation by strings.
The problem I face is, that property_of_a can not be initialized, because ModelB is not declared at that moment.
Here is a simplified (not working) example.
A working alternative for this example might use sa.ext.associationproxy
I do not think, that I can use association proxies because I'd like to be able to use CONCAT, GROUP_CONCAT and IF-THEN-ELSE Queries.
Is there a way to initialize a sa.select query lazy (for example by strings like sa.orm.relationship)
import sqlalchemy as sa
Base = sa.ext.declarative.declarative_base()
metadata = Base.metadata
class ModelA(Base):
__tablename__ = "model_a_table"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(255), nullable=False, unique=True)
model_b_id = sa.Column(sa.ForeignKey('model_b_table.id', ondelete='SET NULL'), index=True)
model_b = sa.orm.relationship("ModelB")
property_of_a = sa.orm.column_property(sa.select([ModelB.name]).where(ModelB.id == model_b_id))
class ModelB(Base):
__tablename__ = "model_b_table"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(255), nullable=False, unique=True)
model_a_id = sa.Column(sa.ForeignKey('model_a_table.id', ondelete='SET NULL'), index=True)
model_b = sa.orm.relationship("ModelA")
property_of_b = sa.orm.column_property(sa.select([ModelA.name]).where(ModelB.id == model_a_id))
sa.orm.configure_mappers()

Related

SQLAlchemy relationship selection criteria

I have two model classes:
class Programs(db.Model):
__tablename__ = "programs"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(100), nullable=False)
duration = db.Column(db.Integer, nullable=False)
date_created = db.Column(db.DATE, default=datetime.now())
created_by = db.Column(db.String(100))
program_sessions = db.relationship('Program_Session',backref='programs')
class Program_Session(db.Model):
__tablename__ = "program_session"
id = db.Column(db.Integer, primary_key=True)
session_title = db.Column(db.String(100), nullable=False)
session_description = db.Column(db.String(100))
session_year = db.Column(db.Integer)
program_id = db.Column(db.Integer, db.ForeignKey("programs.id"), nullable=False)
students = db.relationship('Student_Registration', backref='program_session')
date_created = db.Column(db.DATE, default=datetime.now())
created_by = db.Column(db.String(100))
I create an object of Programs with:
program = Programs.query.first()
Now I can access all the Program_Sessions from the selected Program:
print(pro.program_sessions)
Is it possible to subquery/query to retrieve only those Program_session in Program whose year is 2021?
Option-1: filter on 'python' (in memory)
Once you get all Program_Sessions (all_sessions = pro.program_sessions), you filter them by sessions_2021 = [item for item in all_sessions if item.session_year == 2021].
Needless to say, this is not efficient at all as lots of data will be loaded from the database to be immediately discarded.
Option2: use Dynamic Relationship Loaders
Define the relationship with lazy="dynamic", which will return a Query and hence you will be able to apply additional criteria to the query:
class Programs(db.Model):
# ...
program_sessions = db.relationship('Program_Session', backref='programs', lazy="dynamic")
program = Programs.query.first()
sessions_2021 = program.program_sessions.filter(Program_Session.year == 2021).all()
Option3: use orm.with_parent [BEST]
sessions_2021 = select(Program_Session).where(with_parent(program, Program_Session.programs)).where(Program_Session.year == 2021)
The answer is yes...
van's answer shows you options for playing with sqlalchemy's query mechanism. But what if you want to write this logic on the Programs class itself? That way anywhere you have a Programs object, you can access the filter.
You can do it in pretty plain python by altering the Programs class like so:
class Programs(db.Model):
__tablename__ = "programs"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(100), nullable=False)
duration = db.Column(db.Integer, nullable=False)
date_created = db.Column(db.DATE, default=datetime.now())
created_by = db.Column(db.String(100))
program_sessions = db.relationship('Program_Session',backref='programs')
'''Here I add a filter that returns only the sessions for a particular year
'''
def program_sessions_by_year(self, year):
return filter(lambda ps: ps.session_year == year, self.program_sessions)
If you care about efficiency, you can get the database to do the filtering for you using a bit more sqlalchemy magic:
from sqlalchemy.orm import object_session
class Programs(db.Model):
__tablename__ = "programs"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(100), nullable=False)
duration = db.Column(db.Integer, nullable=False)
date_created = db.Column(db.DATE, default=datetime.now())
created_by = db.Column(db.String(100))
program_sessions = db.relationship('Program_Session',backref='programs')
'''Improve efficiency by using DB's SQL engine to filter the object.
'''
def program_sessions_by_year(self, year):
return object_session(self)\
.query(Program_Session)\
.filter_by(session_year=year, program_id=self.id)\
.all()
Either way you can then write (where-ever you have a Program object):
# lets say you just want the first program
first_program = Programs.query.first()
# to get the program sessions by year 2021
first_program.program_sessions_by_year(2021)
There's probably a bunch of other ways you could do something like this. SqlAlchemy is a big library. For more background on my answer, have a look at the SQL expressions as Mapped Attributes docs.

Can I specify the join conditions of a relationship when writing a query instead of doing so in my model class?

I have three models...
class Customer(db.Model, TimestampMixin):
pk = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text, nullable=False)
class User(db.Model, TimestampMixin):
pk = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text, nullable=False)
class CustomerUserStat(db.Model):
__table_args__ = (
UniqueConstraint("customer_pk", "user_pk", name="customer_pk_user_pk"),
)
pk = db.Column(db.Integer, primary_key=True)
customer_pk = db.Column(db.Integer, db.ForeignKey("customers.pk"), nullable=False)
user_pk = db.Column(db.Integer, db.ForeignKey("users.pk"), nullable=False)
data = db.Column(db.Integer, nullable=False, default=0)
I’ve tried...
m.Customer.query.outerjoin(
m.CustomerUserStat,
(m.CustomerUserStat.user_pk == entity.pk)
& (m.CustomerUserStat.customer_pk == m.Customer.pk)
).add_entity(m.CustomerUserStat)
But that gives me a tuple with a Customer object and a CustomerUserStat (or None if one doesn’t exist), close but not quite what I am looking for.
I’ve also tried adding a userstats relationship to the Customer model and ...
m.Customer.query.outerjoin(m.Customer.userstats
).filter(m.CustomerUserStat.user_pk == user.pk
).options(contains_eager(m.Customer.userstats))
But that didn’t produce any results if the user's CustomerUserStat record was missing.
Ultimately, I would like to write a query that will give me a list of Customer objects with an attribute that has an instance of CustomerUserStat for a user that I specify in the query and I need to be able to order by the CustomerUserStat.data field and paginate the results.
It looks to me like you are looking for the AssociationProxy pattern
I came up with this hack to get the query that I needed. In my Flask view function, I defined a new class that inherits from the original and added the relationship to it. Then I use the new class to create the query.
class TempCustomer(m.Customer):
userstats = db.relationship(
m.CustomerUserStat,
primaryjoin=(m.Customer.pk == m.CustomerUserStat.customer_pk) &
(m.CustomerUserStat.user_pk == agent.pk),
uselist=False,
viewonly=True,
lazy="joined",
)
q = TempCustomer.query.outerjoin(m.CustomerUserStat, (m.Customer.pk ==
m.CustomerUserStat.customer_pk) & (m.CustomerUserStat.user_pk == agent.pk),)
This works for me, but doesn't seem like the best solution.
It also results in two LEFT JOINS for the same data which is probably not the most efficient.

Accessing extra column in many-to-many relationship with simplified association objects

I'm trying to work with the example in the SQLAlchemy docs: Simplifying Association Objects
What I am struggling with understanding is how I can access the special_key. Ultimately I'd like to be able to do something like this:
for user in users
for keyword in user.keywords
keyword.special_key
Here is the code from the example:
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)
Am I on the right track in following this pattern here?
My goal is essentially many-to-many with an extra column containing a boolean value.
This should work:
for user in users:
for keyword in user.user_keywords:
print keyword.special_key

Many-to-many self-referential relationship in sqlalchemy

I'm trying to make a self-referential many-to-many relationship (it means that Line can have many parent lines and many child lines) in sqlalchemy like this:
Base = declarative_base()
class Association(Base):
__tablename__ = 'association'
prev_id = Column(Integer, ForeignKey('line.id'), primary_key=True)
next_id = Column(Integer, ForeignKey('line.id'), primary_key=True)
class Line(Base):
__tablename__ = 'line'
id = Column(Integer, primary_key = True)
text = Column(Text)
condition = Column(Text)
action = Column(Text)
next_lines = relationship(Association, backref="prev_lines")
class Root(Base):
__tablename__ = 'root'
name = Column(String, primary_key = True)
start_line_id = Column(Integer, ForeignKey('line.id'))
start_line = relationship('Line')
But I get the following error:
sqlalchemy.exc.ArgumentError: Could not determine join condition between parent/
child tables on relationship Line.next_lines. Specify a 'primaryjoin' expressio
n. If 'secondary' is present, 'secondaryjoin' is needed as well.
Do you know how I could remedy this?
You should just need:
prev_lines = relationship(
Association,
backref="next_lines",
primaryjoin=id==Association.prev_id)
Since this specifies the next_lines back reference there is no need to have a next_lines relationship.
You can also do this using the remote_side parameter to a relationship: http://www.sqlalchemy.org/trac/browser/examples/adjacency_list/adjacency_list.py

How to insert the 'answer' model?

The title may be not exactly, but I don't know how to express it.
I have 3 class: User, Question, Answer. The simple code is:
Session = scoped_session(sessionmaker())
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
questions = relationship('Question', backref="user")
answers = relationship('Answer', backref="user")
class Question(Base):
__tablename__ = 'questions'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
answers = relationship('Answer', backref="user")
class Answer(Base):
__tablename__ = 'answers'
user_id = Column(Integer, ForeignKey('users.id'))
question_id = Column(Integer, ForeignKey('questions.id'))
id = Column(Integer, primary_key=True)
Now, A user asked a question, so there will be an answer created:
user = get_user_from_session()
question = get_question(question_id)
# create answer
answer = Answer()
answer.user = user
answer.question = question
Session.add(answer) # !!!
Session.commit()
I hope the answer will be inserted to database, but unfortunately, there is an error reported:
AttributeError: 'module' object has no attribute '_sa_instance_state'
Is there something I've missed? How to fix it?
UPDATE
Thanks for #dhaffey, I've fixed the typos. I recreate a test file to test this, found no error happened again, but answer.user_id and answer.question_id are null in database after commit.
This is my code, you can run it directly.
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import *
engine = create_engine('sqlite:///test.sqlite', echo=True)
Session = scoped_session(sessionmaker())
Base = declarative_base()
Base.metadata.bind=engine
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
questions = relationship('Question')
answers = relationship('Answer')
class Question(Base):
__tablename__ = 'questions'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
title = Column(String)
answers = relationship('Answer')
class Answer(Base):
__tablename__ = 'answers'
user_id = Column(Integer, ForeignKey('users.id'))
question_id = Column(Integer, ForeignKey('questions.id'))
id = Column(Integer, primary_key=True)
Base.metadata.create_all()
user = User()
user.name = 'aaa'
Session.add(user)
Session.flush()
question = Question()
question.title = 'ttt'
question.user = user
Session.add(question)
Session.flush()
answer = Answer()
answer.user = user
answer.question = question
Session.add(answer)
Session.commit()
print answer.id # not None
found = Session.query(Answer).filter_by(id=answer.id).one()
print found.user.name # not None
print found.question.title # not None
# !!! It seems all models are saved correctly,
# but please open the test.sqlite database, (not querying by sqlahchemy)
# the question.user_id and answer.user_id and answer.question_id are null
Your class declarations don't "compile" for me, so I'm wondering if you've run this code, and which SQLAlchemy version you're using if so. The line
user_id = Column(Integer, ForeignKey='users.id')
raises
sqlalchemy.exc.ArgumentError: Unknown arguments passed to Column: ['ForeignKey']
with SQLAlchemy 0.6.4. You're trying to declare the foreign key with a keyword argument, but the correct usage is to construct a ForeignKey object and pass it positionally, like this:
user_id = Column(Integer, ForeignKey('users.id'))
With the foreign keys fixed, your example works as expected for me.
Note that you don't need to explicitly provide the primaryjoin argument on these relationships when the corresponding foreign keys are appropriately declared - SQLAlchemy infers the correct join.