I've got two classes that are connected through a many-to-many relationship: Parent and Tag.
Base = declarative_base()
association_table = Table('associations', Base.metadata,
Column('parent_id', Integer, ForeignKey('parent.id')),
Column('tag_id', Integer, ForeignKey('tag.id')),
)
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, Sequence('tag_id_seq'), primary_key=True)
name = Column(String)
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, Sequence('parent_id_seq'), primary_key=True)
tags = relationship('Tag', secondary=association_table, backref='parents')
If I want to query for all the Tag objects that have one or more relationships to a Parent, I'd do:
session.query(Tag).filter(Tag.parents.any()).all()
However, this Parent class is parent to child classes, Alice and Bob:
class Alice(Parent):
__tablename__ = 'alices'
__mapper_args__ = {'polymorphic_identity': 'alice'}
alice_id = Column(Integer, ForeignKey('parents.id'), primary_key=True)
class Bob(Parent):
__tablename__ = 'bobs'
__mapper_args__ = {'polymorphic_identity': 'bob'}
bob_id = Column(Integer, ForeignKey('parents.id'), primary_key=True)
Now I'd like to be able to retrieve all the Tag objects that have one or more relations to an Alice object. The previous query, session.query(Tag).filter(Tag.parents.any()).all(), would not do as it doesn't discriminate between Alice or Bob objects - it doesn't even know about their existence.
I've messed around with Query's for a while with no success so I assume that if it can be done, it must have something to do with some extra lines of code in the Table classes like those shown above. While the documentation holds info about polymorphic classes and many-to-many relations and Mike Bayer himself offered someone this answer to a seemingly related question which looks interesting but which I'm far from understanding, I'm kind of stuck.
The code samples may disgust the Python interpreter, but does hopefully get my point across. Candy for those who can help me on my way!
While writing a little MWE I actually found a solution, which is actually almost the same as what I had tried already. budder gave me new hope for the approach though, thanks :)
from sqlalchemy import create_engine, ForeignKey, Column, String, Integer, Sequence, Table
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
association_table = Table('associations', Base.metadata,
Column('parent_id', Integer, ForeignKey('parents.id')),
Column('tag_id', Integer, ForeignKey('tags.id')),
)
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, Sequence('tag_id_seq'), primary_key=True)
name = Column(String)
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, Sequence('parent_id_seq'), primary_key=True)
tags = relationship('Tag', secondary=association_table, backref='parents')
class Alice(Parent):
__tablename__ = 'alices'
__mapper_args__ = {'polymorphic_identity': 'alice'}
alice_id = Column(Integer, ForeignKey('parents.id'), primary_key=True)
class Bob(Parent):
__tablename__ = 'bobs'
__mapper_args__ = {'polymorphic_identity': 'bob'}
bob_id = Column(Integer, ForeignKey('parents.id'), primary_key=True)
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
tag_a = Tag(name='a')
tag_b = Tag(name='b')
tag_c = Tag(name='c')
session.add(tag_a)
session.add(tag_b)
session.add(tag_c)
session.commit()
session.add(Alice(tags=[tag_a]))
session.add(Bob(tags=[tag_b]))
session.commit()
for tag in session.query(Tag).\
filter(Tag.parents.any(Parent.id == Alice.alice_id)).\
all():
print(tag.name)
If there's a good alternative approach, I'd still be interested. I can imagine sqlalchemy offering a more direct and elegant approach so that one could do, for example:
for tag in session.query(Tag).\
filter(Tag.alices.any()).\
all():
print(tag.name)
If you write your object classes a certain way you can use the blunt force method...Just search for them...Kind of like a crude query method:
all_tag_objects = session.query(Tag).all() ## All tag objects in your database
tags = []
for tag in all_tag_objects:
for parent in tag.parents:
if parent.alices != []: ## If there are alice objects in the tag parents alice reltionship instance variable...Then we append the tag because it meets our criteria.
flagged_tags.append(tag)
Sounds like you found a better way though, but I guess the ultimate test would be to actually do a speed test.
Related
This example develops on the Parent/Child association table on the SQLAlchemy docs (https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#association-object)
How can I define the 'son' or 'daughter' attribute on the Parent class to filter the association table to the appropriate child type table?
I understand how the 'children' relationship works, but I would like to add extra conditionals to the relationship. I suspect this involves the use of secondary in the definition of relationship, but this is at the edge of my understanding of relationships in SQLAlchemy.
from sqlalchemy import Table, Column, Integer, ForeignKey, String
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Association(Base):
__tablename__ = 'association'
left_id = Column(Integer, ForeignKey('left.id'), primary_key=True)
right_id = Column(Integer, ForeignKey('right.id'), primary_key=True)
child = relationship("Child", back_populates="parents")
parent = relationship("Parent", back_populates="children")
child_type = Column(Integer, ForeignKey('child_type'))
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Association", back_populates="parent")
# I would also like to define 'sons' and 'daughters' here
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
parents = relationship("Association", back_populates="child")
class ChildType(Base):
__tablename__ = 'child_type'
id = Column(Integer, primary_key=True)
name = Column(String(50))
son_type = ChildType(name='son')
daughter_type = ChildType(name='daughter')
dad = Parent()
son = Child()
dad_son = Association(child_type=son_type)
dad_son.child = son
dad.children.append(dad_son)
daughter = Child()
dad_daughter = Association(child_type=daughter_type)
dad_daughter.child = daughter
dad.children.append(dad_daughter)
Filtering the association table in the secondary to only include rows of the type that you are interested in for each relationship seems to work:
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Association", back_populates="parent")
# I would also like to define 'sons' and 'daughters' here
sons = relationship(
'Child',
secondary="join(Association, ChildType, "
"and_(Association.child_type_id==ChildType.id, "
"ChildType.name=='son'))",
viewonly=True
)
daughters = relationship(
'Child',
secondary="join(Association, ChildType, "
"and_(Association.child_type_id==ChildType.id, "
"ChildType.name=='daughter'))",
viewonly=True
)
... but adding objects via those relationships won't work as they don't know how to construct the association object with the child_type, hence the viewonly. Also, I changed Association.child_type to child_type_id = Column(Integer, ForeignKey('child_type.id')) which is why that's different in the example.
The Child/ChildType thing stands out as a possible application of the 'Mapping Class Inheritance Hierarchies' pattern, if you aren't locked into this schema, that might be something to explore.
This has been driving me crazy and I'm sure it is more straight forward than I'm making it. But so far, no end of searching and trying various combinations, has failed to get me anywhere.
I'm using Flask and SQLAlchemy and I'm new to both..
So, if i have two classes/tables defined like this
class Child(db.Modal):
__tablename__ = 'children'
id = db.Column(db.Integer, primary_key=True)
kidsname = db.Column(db.String(20))
parent_id = db.Column(db.Integer, db.ForeignKey('parent.id'), nullable=False)
class Parent(db.Model):
__tablename__ = 'parent'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
children = db.relationship('Child', backref='parent', lazy=True)
What I want is to be able to add a property to the parent class to return the number of children e.g.
def child_count(self):
return ????
I would like to be able to do something like this when an instance of parent is passed though to my html template..
<p>Parent: {{ parent.name }} # of children: {{parent.child_count}}</p>
Any help or pointer in the right direction would be greatly appreciated..
You can use column_property https://docs.sqlalchemy.org/en/latest/orm/mapped_sql_expr.html
from sqlalchemy.orm import column_property
from sqlalchemy import select, func
class Parent(db.Model):
__tablename__ = 'parent'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
children = db.relationship('Child', backref='parent', lazy=True)
child_count = column_property(select([func.count(children.id)]).\
where(children.parent_id==id))
Also, you can find a solution to your problem with the hybrid_property in this question SQLAlchemy - Writing a hybrid method for child count
When I try SQLAlchemy Relation Example following this guide: Basic Relationship Patterns
I have this code
#!/usr/bin/env python
# encoding: utf-8
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///:memory:', echo=True)
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base(bind=engine)
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Child")
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
parent = relationship("Parent")
Base.metadata.create_all()
p = Parent()
session.add(p)
session.commit()
c = Child(parent_id=p.id)
session.add(c)
session.commit()
print "children: {}".format(p.children[0].id)
print "parent: {}".format(c.parent.id)
It works well, but in the guide, it says the model should be:
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
**children = relationship("Child", back_populates="parent")**
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
**parent = relationship("Parent", back_populates="children")**
Why don't I need back_populates or backref in my example? When should I use one or the other?
If you use backref you don't need to declare the relationship on the second table.
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Child", backref="parent")
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
If you're not using backref, and defining the relationship's separately, then if you don't use back_populates, sqlalchemy won't know to connect the relationships, so that modifying one also modifies the other.
So, in your example, where you've defined the relationship's separately, but didn't provide a back_populates argument, modifying one field wouldn't automatically update the other in your transaction.
>>> parent = Parent()
>>> child = Child()
>>> child.parent = parent
>>> print(parent.children)
[]
See how it didn't automatically fill out the children field?
Now, if you supply a back_populates argument, sqlalchemy will connect the fields.
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parent.id'))
parent = relationship("Parent", back_populates="children")
So now we get
>>> parent = Parent()
>>> child = Child()
>>> child.parent = parent
>>> print(parent.children)
[Child(...)]
Sqlalchemy knows these two fields are related now, and will update each as the other is updated. It's worth noting that using backref will do this, too. Using back_populates is nice if you want to define the relationships on every class, so it's easy to see all the fields just be glancing at the model class, instead of having to look at other classes that define fields via backref.
Situation
I have the Self-Referential Many-to-Many Relationship (almost identical to the sqlalchemy manual entry of the same heading). This relationship is governed by the table entity_weights. This code works!
Question
How do I include an attribute in class Entity representing the table column entity_weights.weight. Lets say the attribute would be called Entity.child_weights. It is important that the rank of Entity.child_entities and Entity.child_weights are identical.
entity_weights = Table('entity_weights', Base.metadata,
Column('id',Integer, primary_key=True),
Column('parent_entity_id',Integer, ForeignKey('entity.id')),
Column('entity_id',Integer, ForeignKey('entity.id')),
Column('weight',Float))
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, primary_key=True)
name = Column(String)
domicile_id = Column(Integer, ForeignKey('domicile.id'))
entity_type = Column('type',Enum('asset','institution','model'))
source_table_id = Column(Integer)
child_entities = relationship('Entity',
secondary=entity_weights,
primaryjoin=id==entity_weights.c.parent_entity_id,
secondaryjoin=id==entity_weights.c.entity_id,
backref='parent_entity_id'
)
The cleanest solution I've found for this scenario is to break up the child_entities relationship by adding entity_weights as a one-to-many relationship on Entity and use an association proxy to proxy the weight value as well as the remote side of the many-to-many relationship:
class EntityWeight(Base):
__tablename__ = "entity_weights"
id = Column(Integer, primary_key=True)
parent_entity_id = Column(Integer, ForeignKey('entity.id'))
entity_id = Column(Integer, ForeignKey('entity.id'))
weight = Column(Float)
entity = relationship("Entity", primaryjoin=lambda: EntityWeight.entity_id == Entity.id)
class Entity(Base):
...
_child_weights = relationship(EntityWeight, primaryjoin=id == EntityWeight.parent_entity_id)
child_weights = association_proxy("_child_weights", "weight")
child_entities = association_proxy("_child_weights", "entity")
I have read the SQLAlchemy documentation and tutorial about building many-to-many relation but I could not figure out how to do it properly when the association table contains more than the 2 foreign keys.
I have a table of items and every item has many details. Details can be the same on many items, so there is a many-to-many relation between items and details
I have the following:
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
My association table is (It's defined before the other 2 in the code):
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
In the documentation, it's said that I need to use the "association object". I could not figure out how to use it properly, since it's mixed declarative with mapper forms and the examples seem not to be complete. I added the line:
details = relation(ItemDetail)
as a member of Item class and the line:
itemDetail = relation('Detail')
as a member of the association table, as described in the documentation.
when I do item = session.query(Item).first(), the item.details is not a list of Detail objects, but a list of ItemDetail objects.
How can I get details properly in Item objects, i.e., item.details should be a list of Detail objects?
From the comments I see you've found the answer. But the SQLAlchemy documentation is quite overwhelming for a 'new user' and I was struggling with the same question. So for future reference:
ItemDetail = Table('ItemDetail',
Column('id', Integer, primary_key=True),
Column('itemId', Integer, ForeignKey('Item.id')),
Column('detailId', Integer, ForeignKey('Detail.id')),
Column('endDate', Date))
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail, backref='Detail')
Like Miguel, I'm also using a Declarative approach for my junction table. However, I kept running into errors like
sqlalchemy.exc.ArgumentError: secondary argument <class 'main.ProjectUser'> passed to to relationship() User.projects must be a Table object or other FROM clause; can't send a mapped class directly as rows in 'secondary' are persisted independently of a class that is mapped to that same table.
With some fiddling, I was able to come up with the following. (Note my classes are different than OP's but the concept is the same.)
Example
Here's a full working example
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, Session
# Make the engine
engine = create_engine("sqlite+pysqlite:///:memory:", future=True, echo=False)
# Make the DeclarativeMeta
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
projects = relationship('Project', secondary='project_users', back_populates='users')
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary='project_users', back_populates='projects')
class ProjectUser(Base):
__tablename__ = "project_users"
id = Column(Integer, primary_key=True)
notes = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
# Create the tables in the database
Base.metadata.create_all(engine)
# Test it
with Session(bind=engine) as session:
# add users
usr1 = User(name="bob")
session.add(usr1)
usr2 = User(name="alice")
session.add(usr2)
session.commit()
# add projects
prj1 = Project(name="Project 1")
session.add(prj1)
prj2 = Project(name="Project 2")
session.add(prj2)
session.commit()
# map users to projects
prj1.users = [usr1, usr2]
prj2.users = [usr2]
session.commit()
with Session(bind=engine) as session:
print(session.query(User).where(User.id == 1).one().projects)
print(session.query(Project).where(Project.id == 1).one().users)
Notes
reference the table name in the secondary argument like secondary='project_users' as opposed to secondary=ProjectUser
use back_populates instead of backref
I made a detailed writeup about this here.
Previous Answer worked for me, but I used a Class base approach for the table ItemDetail. This is the Sample code:
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True, index=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail.__table__, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail.__table__, backref='Detail')