Optionally reference a single table from multiple other tables - python

I'm a total beginner to Flask and SQLAlchemy and I'm stuck on what should be quite a simple problem.
My application includes the following two tables:
class Item(db.Model):
id = db.Column(UUIDType(), primary_key=True)
# more columns
class Person(db.Model)
id = db.Column(UUIDType(), primary_key=True)
# more columns
Unnecessary columns have been removed for brevity. I'm required to add a third kind of table, Description, which has a number of properties. Item and Person can optionally have descriptions.
This is what I've done:
class Description(db.Model):
id = db.Column(UUIDType(), primary_key=True)
text = db.Column(db.BLOB, nullable=False)
# some other properties
class Item(db.Model):
id = db.Column(UUIDType(), primary_key=True)
description_id = db.Column(UUIDType(), db.ForeignKey('tags.id'), nullable=True)
description = db.relationship('Description')
class Person(db.Model)
id = db.Column(UUIDType(), primary_key=True)
description_id = db.Column(UUIDType(), db.ForeignKey('tags.id'), nullable=True)
description = db.relationship('Description')
I'm not sure if this is the right way to approach the problem, particularly since the description_id column is nullable but the description relationship is not.
Is this correct? And if so (doubtful), is there a better way to solve the problem?

Related

Two levels deep one-to-many relationship with contains()

I have two tables like so
class Department(Model):
id = Column(Integer, primary_key=True)
employees = relationship('Employee', backref='department')
class Employee(Model):
id = Column(Integer, primary_key=True)
name = Column(String)
department_id = Column(Integer, ForeignKey('department.id'))
email_addresses = relationship('EmailAddress', backref='employee')
class EmailAddress(Model):
id = Column(Integer, primary_key=True)
value = Column(String)
employee_id = Column(Integer, ForeignKey('employee.id'))
And now I need to find all departments that have an employee with an email address of X.
Back when each employee was allowed to have only one email address (so they had email_address = Column(String) I used this code, but I'm not sure how to extend it to work with multiple emails:
session.query(Department).filter(
Employee.email_address==request.args['email']
).outerjoin((Employee, Department.employees))
You can use the any() method of relationship. This code should do it:
session.query(Department).filter(
Employee.email_address.any(value=request.args['email'])
).outerjoin((Employee, Department.employees))
You could use any() like this :
department1 = Department()
department2 = Department()
DBSession.add(department1)
DBSession.add(department2)
employee = Employee()
employee.email_addresses.append(EmailAddress(employee.id, 'e#mail.com'))
DBSession.add(employee)
department1.employees.append(employee)
DBSession.flush()
print(DBSession.query(Department).filter(Department.employees.any(Employee.email_addresses.any(EmailAddress.value == 'e#mail.com'))).all())
see this gist for a full example.

How can I construct a count aggregation over a join with SqlAlchemy?

I have a table of users, a table of groups that those users may belong to, and a join table between users and groups.
This is represented in SQLAlchemy as follows:
class User(Base):
__tablename__ = 'user'
user_id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
email = Column(String(250), nullable=False)
groups = relationship('Group', secondary='user_group_pair')
class Group(Base):
__tablename__ = 'group'
group_id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
date_created = Column(String(250), nullable=False)
members = relationship('User', secondary='user_group_pair')
class User_Group_Pair(Base):
__tablename__ = 'user_group_pair'
user_group_pair_id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('user.user_id'))
group_id = Column(Integer, ForeignKey('group.group_id'))
user = relationship(User, backref=backref("group_assoc"))
group = relationship(Group, backref=backref("user_assoc"))
I'm trying to solve the following simple problem:
I want to write a query that will return a list of users along with the number of groups that each of them belongs to.
This requires data from both User and User_Group_Pair (thus why the title of my question refers to a join), and a count aggregation grouped by user_id.
I'm not sure why this won't work:
subq = session.query(User_Group_Pair.user_id.label('user_id'), func.count(User_Group_Pair.user_group_pair_id).label('count')).\
group_by(User_Group_Pair.user_id).order_by('count ASC').subquery()
result = session.query(User).join(subq, User.user_id == subq.user_id).all()
I get this error:
'Alias' object has no attribute 'user_id'
However, note that I have labelled User_Group_Pair.user_id with the label 'user_id'... Any thoughts?
Thank you
Just change subq.user_id to subq.c.user_id (c stands for columns) to make it work:
result = session.query(User).join(subq, User.user_id == subq.c.user_id).all()
But still you will get only those users which belong to at least one group, and the number of groups is not really returned in the result of the query. The query below is an approach to solve this issue:
q = (session.query(User, func.count(Group.group_id).label("num_groups"))
.outerjoin(Group, User.groups)
.group_by(User.user_id)
)
for b, num_groups in q:
print(b, num_groups)
http://docs.sqlalchemy.org/en/rel_1_0/orm/tutorial.html#using-subqueries
subquery() method on Query produces a SQL expression construct representing a SELECT statement embedded within an alias. The columns on the statement are accessible through an attribute called c.
You can use column names with .c.column_name in your query
result = session.query(User).join(subq, User.user_id == subq.c.user_id).all()

Using sqlalchemy to define relationships in MySQL

I am in the process of working with sqlalchemy and MySQL to build a database. I am currently having trouble defining a specific relationship between two tables.
class Experiment_Type(Base):
__tablename__='experiment_types'
id = Column(Integer, primary_key=True)
type = Column(String(100))
class Experiments(Base):
__tablename__ = 'experiments'
id = Column(Integer, primary_key=True)
type = Column(String(100))
sub_id = Column(Integer, ForeignKey('experiment_types.id'))
experiments = relationship('Experiments',
primaryjoin="and_(Experiment_Type.id == Experiments.sub_id,
'Experiments.type' == 'Experiment_Type.type')",
backref=backref('link'))
What I want to do is have values of sub_id in experiments match the id in experiment_types based on type (if an entry in experiment_types of type = 'type1' has id = 1, then an entry in experiments with type = 'type1' should have a sub_id = 1). I am not even sure if this is the best way to approach defining the relationship in this situation
so any advice is welcome.
The current error message is this:
sqlalchemy.exc.ArgumentError: Could not locate any relevant foreign key columns for primary join condition '0 = 1' on relationship Experiments.experiments. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or are annotated in the join condition with the foreign() annotation.
The whole point of setting up relationships in relational dbs is to not have to duplicate data across tables. Just do something like this:
class ExperimentType(Base):
__tablename__='experiment_types'
id = Column(Integer, primary_key=True)
name = Column(String(100))
class Experiments(Base):
__tablename__ = 'experiments'
id = Column(Integer, primary_key=True)
description = Column(String(100))
type_id = Column(Integer, ForeignKey('experiment_types.id'))
type = relationship("ExperimentType")
Then, if you do need to display the experiment type stuff later, can access it with something like:
exp = session.query(Experiment).first()
print exp.type.name

How to construct different-type-ones-to-many relationship?

If I have two tables: A (id, ... some other columns) and B (id, ... some other columns). I need to associate Comments (another table: id, text, author) with A and B objects.
Can I do something like creating the fourth table for that purpose: comment_id, table_id (A or B or maybe others), item_id? I mean some sqlalchemy way?
Now I know only of those two solutions:
http://bpaste.net/show/27149/ - for each A and B there will be separate table with comments. I don't think it's a good idea, because those tables are (should be) identical, if I want to see all comments by some author - it will be more difficult than the other ways, if there will be C table someday - I will need to create a table for its comments...
Another solution - http://bpaste.net/show/27148/. I think is better, but I still will be needing to create an association table for every table I need to comment the items.
Any ideas? Thanks in advance.
You could try:
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey('a.id'), nullable=True)
b_id = Column(Integer, ForeignKey('b.id'), nullable=True)
text = Column(UnicodeText, nullable=False)
def __init__(self, text):
self.text = text
It is still a relatively hacky way to do it, but it preserves referential integrity by using foreign keys.
If you'd like to go for the solution where you store the table name, you could try something like this:
class A(Base):
__tablename__ = 'as'
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False, unique=True)
comments = relationship("Comment",
primaryjoin="and_(A.id==Comment.id, "
"Comment.model=='A')")
def __init__(self, name):
self.name = name
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
text = Column(UnicodeText, nullable=False)
model = Column(String(50), nullable=False)
def __init__(self, text):
self.text = text
I have not tested this myself, so if you run into problems have a look at Specifying Alternate Join Conditions or comment and I will do more research.
For an in-depth explanation of the subject, see Mike Bayer's blog entry on Polymorphic Associations
I would go for the second solution too. But, as you say there might be a third table C, then you could put the B_Comment and A_Comment as fields in one single association table, like:
class Comment(Base):
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey('as.id'), primary_key=True, nullable=True)
b_id = Column(Integer, ForeignKey('bs.id'), primary_key=True, nullable=True)
text = Column(UnicodeText, nullable=False)
# ...
And you would use a query like: session.query(Comment).filter_by(a=whatever)
I guess you could add some kind of a constraint so that a_id and b_id are not simultaneously NULL ?
Here's another way to do it (I don't know if it's the standard way to do it, but it should work...
class Letter(Base):
id = ...
class A(Base):
letter_id = Column(Integer, ForeignKey('letters.id'), primary_key=True, nullable=False)
# ...
class Comment(Base):
letter_id = Column(Integer, ForeignKey('letters.id'), primary_key=True, nullable=True)
# ...

Do I need multiple association tables for this relationship?

I'm trying to get my head around the best way to construct a relationship that maps many Constants to many Items.
My initial relationship, an Item has a Constant, looked like this.
class Constant(Base):
__tablename__ = "Constant"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(64), nullable=False)
class Item(Base):
__tablename__ = "Item"
id = Column(Integer, primary_key=True, autoincrement=True)
constantId = Column(Integer, ForeignKey("Constant.id"))
constant = relationship("Constant")
However, I really need my item to have more than one constant, something like this...
class Item(Base):
__tablename__ = "Item"
id = Column(Integer, primary_key=True, autoincrement=True)
constant1Id = Column(Integer, ForeignKey("Constant.id"))
constant1 = relationship("Constant")
constant2Id = Column(Integer, ForeignKey("Constant.id"))
constant2 = relationship("Constant")
My first attempt was to use an association table...
item_to_constant_assoc = Table("itemToConstantAssoc", Base.metadata, Column("constantId", Integer, ForeignKey("Constant.id"), Column("itemId", Integer, ForeignKey("Item.id")))
while updating the Item class accordingly:
Class Item(Base):
__tablename__ = "Item"
id = Column(Integer, primary_key=True, autoincrement=True)
constant1 = relationship("Constant", secondary=item_to_constant_assoc, uselist=False)
constant2 = relationship("Constant", secondary=item_to_constant_assoc, uselist=False)
This failed (understandably when looking at the MySQL tables that were created) because Item.constant1 and Item.constant2 referred to the same entry in the association table.
My next step is to add another association table for the second constant but I have to wonder whether I'm barking up the wrong tree as I seem to be creating a large number of tables for a relatively simple mapping. I've read the documentation. It is detailed and substantial (thanks Michael Bayer!) and I may have just overlooked a section. Could anyone provide me with a few pointers either to this problem, or what I should be looking for in the docs?
Thanks!
Phil
Couldn't see the wood for the trees. This is easily accomplished by using the primaryjoin argument on the relationship.
class Item(Base):
__tablename__ = "Item"
id = Column(Integer, primary_key=True, autoincrement=True)
constant1Id = Column(Integer, ForeignKey("Constant.id"))
constant1 = relationship("Constant", primaryjoin="Constant.id==Item.constant1Id")
constant2Id = Column(Integer, ForeignKey("Constant.id"))
constant2 = relationship("Constant", primaryjoin="Constant.id==Item.constant2Id")
A many-to-many association already allows each Item to have an unlimited number of constants. You don't need anything more than this as your two base tables.
class Constant(Base):
__tablename__ = "Constant"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(64), nullable=False)
class Item(Base):
__tablename__ = "Item"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(64), nullable=False)
item_to_constant_assoc = Table("itemToConstantAssoc", Base.metadata, Column("constantId", Integer, ForeignKey("Constant.id"), Column("itemId", Integer, ForeignKey("Item.id")))
At this point, every Item has an unlimited number of Constants. When you want a specific constant, you have to query the constant by the name attribute in the Constant table. Your association table is merely a list of key pairs: (itemID, constantId).
The set of all Constants for an Item is a three-table join for all association rows joined with matching Constant rows for a given Item.
The set of all Items for a Constant is a three-table join for all association rows join with match Item rows for a given Constant.
A specific Constant for an Item needs to be retrieved via a join. You think of it like the the set of all Constants for a given Item where both the Item and the Constant name are given. The SQL involves a join even though only a single row is retrieved.
I think your generic query to associate a constant with all relevant items or an item with all relevant constants will look something like this.
query(Item). join(item_to_constant_assoc.itemId==Item.itemId). join(item_to_constant_assoc.contantId==Constant.constantId

Categories

Resources