通过 SqlAlchemy 中的关联对象实现多对多、自引用、非对称关系(twitter 模型)
Many-to-many, self-referential, non-symmetrical relationship (twitter model) via Association Object in SqlAlchemy
如何在 SqlAlchemy 中最好地实现多对多、自引用、非对称关系(想想 Twitter)?我想使用一个关联对象(我们称之为 class "Follow"),这样我就可以拥有与该关系关联的其他属性。
我见过很多使用关联表的例子,但没有一个像我上面描述的那样。这是我目前拥有的:
class UserProfile(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
full_name = Column(Unicode(80))
gender = Column(Enum(u'M',u'F','D', name='gender'), nullable=False)
description = Column(Unicode(280))
followed = relationship(Follow, backref="followers")
class Follow(Base):
__tablename__ = 'follow'
follower_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
followee_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
status = Column(Enum(u'A',u'B', name=u'status'), default=u'A')
created = Column(DateTime, default=func.now())
followee = relationship(UserProfile, backref="follower")
想法?
这已经 几乎 在 here 中回答了。在这里,通过使用 link table 的 多对多 的优势,这得到了改进。
我不擅长 SQL,也不擅长 SqlAlchemy,但由于我对这个问题有较长时间的想法,所以我试图找到一个具有两个优点的解决方案:具有附加属性的关联对象和一个 direct 关联,就像一个裸 link table (它本身不为关联提供对象)。受到 op 的其他建议的刺激,以下对我来说似乎很不错:
#!/usr/bin/env python3
# coding: utf-8
import sqlalchemy as sqAl
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy
engine = sqAl.create_engine('sqlite:///m2m-w-a2.sqlite') #, echo=True)
metadata = sqAl.schema.MetaData(bind=engine)
Base = declarative_base(metadata)
class UserProfile(Base):
__tablename__ = 'user'
id = sqAl.Column(sqAl.Integer, primary_key=True)
full_name = sqAl.Column(sqAl.Unicode(80))
gender = sqAl.Column(sqAl.Enum('M','F','D', name='gender'), default='D', nullable=False)
description = sqAl.Column(sqAl.Unicode(280))
following = association_proxy('followeds', 'followee')
followed_by = association_proxy('followers', 'follower')
def follow(self, user, **kwargs):
Follow(follower=self, followee=user, **kwargs)
def __repr__(self):
return 'UserProfile({})'.format(self.full_name)
class Follow(Base):
__tablename__ = 'follow'
followee_id = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
follower_id = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
status = sqAl.Column(sqAl.Enum('A','B', name=u'status'), default=u'A')
created = sqAl.Column(sqAl.DateTime, default=sqAl.func.now())
followee = relationship(UserProfile, foreign_keys=followee_id, backref='followers')
follower = relationship(UserProfile, foreign_keys=follower_id, backref='followeds')
def __init__(self, followee=None, follower=None, **kwargs):
"""necessary for creation by append()ing to the association proxy 'following'"""
self.followee = followee
self.follower = follower
for kw,arg in kwargs.items():
setattr(self, kw, arg)
Base.metadata.create_all(engine, checkfirst=True)
session = sessionmaker(bind=engine)()
def create_sample_data(sess):
import random
usernames, fstates, genders = ['User {}'.format(n) for n in range(4)], ('A', 'B'), ('M','F','D')
profs = []
for u in usernames:
user = UserProfile(full_name=u, gender=random.choice(genders))
profs.append(user)
sess.add(user)
for u in [profs[0], profs[3]]:
for fu in profs:
if u != fu:
u.follow(fu, status=random.choice(fstates))
profs[1].following.append(profs[3]) # doesn't work with followed_by
sess.commit()
# uncomment the next line and run script once to create some sample data
# create_sample_data(session)
profs = session.query(UserProfile).all()
print( '{} follows {}: {}'.format(profs[0], profs[3], profs[3] in profs[0].following))
print('{} is followed by {}: {}'.format(profs[0], profs[1], profs[1] in profs[0].followed_by))
for p in profs:
print("User: {0}, following: {1}".format(
p.full_name, ", ".join([f.full_name for f in p.following])))
for f in p.followeds:
print(" " * 25 + "{0} follow.status: '{1}'"
.format(f.followee.full_name, f.status))
print(" followed_by: {1}".format(
p.full_name, ", ".join([f.full_name for f in p.followed_by])))
for f in p.followers:
print(" " * 25 + "{0} follow.status: '{1}'"
.format(f.follower.full_name, f.status))
Association Object. The association_proxy method seems to be not ideally tailored for self-referential relations. The argument oder of the Follow
constructor doesn't seem logical to me but works only this way (this is explained here).
定义两个关系似乎是必不可少的
在书 Rick Copeland - Essential Sqlalchemy 的第 117 页,您可以找到以下关于 relationship()
的 secondary
参数的注释:
Note that, if you are using SQLAlchemy’s ability to do M:N
relationships, the join table should only be used to join the two
tables together, not to store auxilliary properties. If you need to
use the intermediate join table to store addi- tional properties of
the relation, you should use two 1:N relations instead.
抱歉,这有点冗长,但我喜欢可以直接复制、粘贴和执行的代码。这适用于 Python 3.4 和 SqlAlchemy 0.9,但也可能适用于其他版本。
如何在 SqlAlchemy 中最好地实现多对多、自引用、非对称关系(想想 Twitter)?我想使用一个关联对象(我们称之为 class "Follow"),这样我就可以拥有与该关系关联的其他属性。
我见过很多使用关联表的例子,但没有一个像我上面描述的那样。这是我目前拥有的:
class UserProfile(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
full_name = Column(Unicode(80))
gender = Column(Enum(u'M',u'F','D', name='gender'), nullable=False)
description = Column(Unicode(280))
followed = relationship(Follow, backref="followers")
class Follow(Base):
__tablename__ = 'follow'
follower_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
followee_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
status = Column(Enum(u'A',u'B', name=u'status'), default=u'A')
created = Column(DateTime, default=func.now())
followee = relationship(UserProfile, backref="follower")
想法?
这已经 几乎 在 here 中回答了。在这里,通过使用 link table 的 多对多 的优势,这得到了改进。
我不擅长 SQL,也不擅长 SqlAlchemy,但由于我对这个问题有较长时间的想法,所以我试图找到一个具有两个优点的解决方案:具有附加属性的关联对象和一个 direct 关联,就像一个裸 link table (它本身不为关联提供对象)。受到 op 的其他建议的刺激,以下对我来说似乎很不错:
#!/usr/bin/env python3
# coding: utf-8
import sqlalchemy as sqAl
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy
engine = sqAl.create_engine('sqlite:///m2m-w-a2.sqlite') #, echo=True)
metadata = sqAl.schema.MetaData(bind=engine)
Base = declarative_base(metadata)
class UserProfile(Base):
__tablename__ = 'user'
id = sqAl.Column(sqAl.Integer, primary_key=True)
full_name = sqAl.Column(sqAl.Unicode(80))
gender = sqAl.Column(sqAl.Enum('M','F','D', name='gender'), default='D', nullable=False)
description = sqAl.Column(sqAl.Unicode(280))
following = association_proxy('followeds', 'followee')
followed_by = association_proxy('followers', 'follower')
def follow(self, user, **kwargs):
Follow(follower=self, followee=user, **kwargs)
def __repr__(self):
return 'UserProfile({})'.format(self.full_name)
class Follow(Base):
__tablename__ = 'follow'
followee_id = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
follower_id = sqAl.Column(sqAl.Integer, sqAl.ForeignKey('user.id'), primary_key=True)
status = sqAl.Column(sqAl.Enum('A','B', name=u'status'), default=u'A')
created = sqAl.Column(sqAl.DateTime, default=sqAl.func.now())
followee = relationship(UserProfile, foreign_keys=followee_id, backref='followers')
follower = relationship(UserProfile, foreign_keys=follower_id, backref='followeds')
def __init__(self, followee=None, follower=None, **kwargs):
"""necessary for creation by append()ing to the association proxy 'following'"""
self.followee = followee
self.follower = follower
for kw,arg in kwargs.items():
setattr(self, kw, arg)
Base.metadata.create_all(engine, checkfirst=True)
session = sessionmaker(bind=engine)()
def create_sample_data(sess):
import random
usernames, fstates, genders = ['User {}'.format(n) for n in range(4)], ('A', 'B'), ('M','F','D')
profs = []
for u in usernames:
user = UserProfile(full_name=u, gender=random.choice(genders))
profs.append(user)
sess.add(user)
for u in [profs[0], profs[3]]:
for fu in profs:
if u != fu:
u.follow(fu, status=random.choice(fstates))
profs[1].following.append(profs[3]) # doesn't work with followed_by
sess.commit()
# uncomment the next line and run script once to create some sample data
# create_sample_data(session)
profs = session.query(UserProfile).all()
print( '{} follows {}: {}'.format(profs[0], profs[3], profs[3] in profs[0].following))
print('{} is followed by {}: {}'.format(profs[0], profs[1], profs[1] in profs[0].followed_by))
for p in profs:
print("User: {0}, following: {1}".format(
p.full_name, ", ".join([f.full_name for f in p.following])))
for f in p.followeds:
print(" " * 25 + "{0} follow.status: '{1}'"
.format(f.followee.full_name, f.status))
print(" followed_by: {1}".format(
p.full_name, ", ".join([f.full_name for f in p.followed_by])))
for f in p.followers:
print(" " * 25 + "{0} follow.status: '{1}'"
.format(f.follower.full_name, f.status))
Association Object. The association_proxy method seems to be not ideally tailored for self-referential relations. The argument oder of the Follow
constructor doesn't seem logical to me but works only this way (this is explained here).
在书 Rick Copeland - Essential Sqlalchemy 的第 117 页,您可以找到以下关于 relationship()
的 secondary
参数的注释:
Note that, if you are using SQLAlchemy’s ability to do M:N relationships, the join table should only be used to join the two tables together, not to store auxilliary properties. If you need to use the intermediate join table to store addi- tional properties of the relation, you should use two 1:N relations instead.
抱歉,这有点冗长,但我喜欢可以直接复制、粘贴和执行的代码。这适用于 Python 3.4 和 SqlAlchemy 0.9,但也可能适用于其他版本。