假设我有一组用户,并且每个用户都可以访问一组工具。同一工具可能具有许多用户访问权限,因此这是多对多关系:
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=False)
user_tool_assoc_table = db.Table('user_tool', db.Model.metadata,
db.Column('user', db.Integer, db.ForeignKey('user.id')),
db.Column('tool', db.Integer, db.ForeignKey('tool.id')))
观察到用户名是唯一的,但工具名不是。因此,User.name:Mike1
和User.name:Mike2
可以访问Tool.name:Hammer
,并且User.name:John1
和User.name:John2
可以分别访问同名的Tool.name:Hammer
但每个都有不同的Tool.ids
。
我想约束一下,在User.tools
集合中,永远不会有一个与另一个名称相同的工具,即
Tool
作为其集合的一部分。 Mike1
无法创建名为Hammer
的新工具,该工具构成其tools
集合的一部分。Tool
,则无法将数据库中存在的tools
附加到用户的Hammer
集合中与Mike1合作,因为Mike1已经拥有自己的Hammer
。James
可以创建一个新的Hammer
,因为他还没有锤子。然后,数据库中将有3个名为Hammer
的工具,每个工具都有一组不同的Users
。Tool
仅在具有至少一个User
的情况下存在,但我也不知道如何在我的数据库中本地确保这一点。SQLalchemy是否可以自动配置数据库以保持完整性?我不想编写自己的验证器规则,因为我可能会遗漏某些东西,并最终得到破坏我的规则的数据库。
答案 0 :(得分:4)
问题在于如何表达谓词“由ID标识的用户只有一个名称为NAME的工具”。当然,使用以下简单表即可轻松表达这一点:
db.Table('user_toolname',
db.Column('user', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('toolname', db.String, primary_key=True))
非常清楚的是,仅凭这一点还不足以维持完整性,因为关于用户工具名的事实与实际工具之间没有任何联系。您的数据库可能会指出用户既有锤子又没有锤子。
最好在您的user_tool_assoc_table
中执行此操作或执行等效操作,但是由于Tool.name
不是Tool
主键的一部分,因此您不能引用它。另一方面,由于您确实希望允许多个具有相同名称的工具共存,因此实际上,子集 {id,name} 是Tool
的正确密钥:>
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, primary_key=True)
id
现在充当具有相同名称的工具之间的某种“区分符”。请注意,id
在此模型中不必是全局唯一的,而在name
中是局部的。使它仍然保持自动递增很方便,但是默认设置autoincrement='auto'
仅将单列整数主键视为具有默认的自动递增行为,因此必须对其进行显式设置。
现在还可以根据user_tool_assoc_table
来定义tool_name
,并且附加的约束是用户只能使用一个具有给定名称的工具:
user_tool_assoc_table = db.Table(
'user_tool',
db.Column('user', db.Integer, db.ForeignKey('user.id')),
db.Column('tool', db.Integer),
db.Column('name', db.String),
db.ForeignKeyConstraint(['tool', 'name'],
['tool.id', 'tool.name']),
db.UniqueConstraint('user', 'name'))
使用此模型和以下设置:
john = User(name='John')
mark = User(name='Mark')
db.session.add_all([john, mark])
hammer1 = Tool(name='Hammer')
hammer2 = Tool(name='Hammer')
db.session.add_all([hammer1, hammer2])
db.session.commit()
这将成功:
john.tools.append(hammer1)
hammer2.users.append(mark)
db.session.commit()
由于上述行为违反了唯一约束,因此上述操作将失败:
john.tools.append(hammer2)
db.session.commit()
答案 1 :(得分:2)
如果要通过允许工具名称不唯一来对域进行建模,则没有简单的方法来完成此操作。
您可以尝试向用户模型添加验证器,该验证器将在每次追加过程中检查User.tools
列表,并确保其符合特定条件
from sqlalchemy.orm import validates
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
@validates('tools')
def validate_tool(self, key, tool):
assert tool.name not in [t.name for t in self.tools]
return tool
def __repr__(self):
return self.name
以上方法将确保如果添加与user.tools
列表中现有工具同名的新工具,它将引发异常。但是问题在于,您仍然可以使用像这样的重复工具直接分配新列表
mike.tools = [hammer1, hammer2, knife1]
这将起作用,因为validates
仅在追加操作期间起作用。不在分配期间。如果我们想要一个即使在分配期间也能使用的解决方案,那么我们将不得不找出一个解决方案,其中user_id
和tool_name
将在同一张表中。
我们可以通过使辅助关联表具有3列user_id
,tool_id
和tool_name
来做到这一点。然后,我们可以使tool_id
和tool_name
一起充当Composite Foreign Key
(请参阅https://docs.sqlalchemy.org/en/latest/core/constraints.html#defining-foreign-keys)
通过这种方法,关联表将具有指向user_id
的标准外键,然后具有将tool_id
和tool_name
组合在一起的复合外键约束。现在关联表中已经有两个键,接下来我们可以在表上定义一个UniqueConstraint
,这将确保user_id
和tool_name
必须是唯一的组合< / p>
这是代码
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.orm import validates
from sqlalchemy.schema import ForeignKeyConstraint, UniqueConstraint
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)
user_tool_assoc_table = db.Table('user_tool', db.Model.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('tool_id', db.Integer),
db.Column('tool_name', db.Integer),
ForeignKeyConstraint(['tool_id', 'tool_name'], ['tool.id', 'tool.name']),
UniqueConstraint('user_id', 'tool_name', name='unique_user_toolname')
)
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
tools = db.relationship("Tool", secondary=user_tool_assoc_table,
back_populates='users')
def __repr__(self):
return self.name
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=False)
users = db.relationship("User", secondary=user_tool_assoc_table,
back_populates='tools')
def __repr__(self):
return "{0} - ID: {1}".format(self.name, self.id)
db.create_all()
mike=User(name="Mike")
pete=User(name="Pete")
bob=User(name="Bob")
db.session.add_all([mike, pete, bob])
db.session.commit()
hammer1 = Tool(name="hammer")
hammer2 = Tool(name="hammer")
knife1 = Tool(name="knife")
knife2 = Tool(name="knife")
db.session.add_all([hammer1, hammer2, knife1, knife2])
db.session.commit()
现在让我们尝试玩耍
In [2]: users = db.session.query(User).all()
In [3]: tools = db.session.query(Tool).all()
In [4]: users
Out[4]: [Mike, Pete, Bob]
In [5]: tools
Out[5]: [hammer - ID: 1, hammer - ID: 2, knife - ID: 3, knife - ID: 4]
In [6]: users[0].tools = [tools[0], tools[2]]
In [7]: db.session.commit()
In [9]: users[0].tools.append(tools[1])
In [10]: db.session.commit()
---------------------------------------------------------------------------
IntegrityError Traceback (most recent call last)
<ipython-input-10-a8e4ec8c4c52> in <module>()
----> 1 db.session.commit()
/home/surya/Envs/inkmonk/local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.pyc in do(self, *args, **kwargs)
151 def instrument(name):
152 def do(self, *args, **kwargs):
--> 153 return getattr(self.registry(), name)(*args, **kwargs)
154 return do
因此,添加相同名称的工具会引发异常。
现在让我们尝试分配一个具有重复工具名称的列表
In [14]: tools
Out[14]: [hammer - ID: 1, hammer - ID: 2, knife - ID: 3, knife - ID: 4]
In [15]: users[0].tools = [tools[0], tools[1]]
In [16]: db.session.commit()
---------------------------------------------------------------------------
IntegrityError Traceback (most recent call last)
<ipython-input-16-a8e4ec8c4c52> in <module>()
----> 1 db.session.commit()
/home/surya/Envs/inkmonk/local/lib/python2.7/site-packages/sqlalchemy/orm/scoping.pyc in do(self, *args, **kwargs)
151 def instrument(name):
152 def do(self, *args, **kwargs):
--> 153 return getattr(self.registry(), name)(*args, **kwargs)
154 return do
这也会引发异常。因此,我们确保在数据库级别上满足您的要求。
但是在我看来,采用这种复杂的方法通常表明我们不必要地使设计复杂化。如果您可以更改表格设计,请考虑以下建议以采用更简单的方法。
在我看来,最好拥有一组独特的工具和一组独特的用户,然后为它们之间的M2M关系建模。任何与Mike的锤子有关但不存在于James锤子中的财产,应属于它们之间的关联。
如果采用这种方法,那么会有这样的一组用户
迈克,詹姆斯,约翰,乔治
和一组类似的工具
锤子,螺丝刀,楔子,剪刀,刀子
您仍然可以为它们之间的多对多关系建模。在这种情况下,您唯一要做的更改是在unique=True
列上设置Tool.name
,这样全局上只有一个锤子可以使用该名称。
如果您需要Mike的锤子来具有一些与James的Hammer不同的独特属性,那么您只需在关联表中添加一些额外的列即可。要访问user.tools和tool.users,可以使用association_proxy。
from sqlalchemy.ext.associationproxy import association_proxy
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
associated_tools = db.relationship("UserToolAssociation")
tools = association_proxy("associated_tools", "tool")
class Tool(db.Model):
__tablename__ = 'tool'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
associated_users = db.relationship("UserToolAssociation")
users = association_proxy("associated_users", "user")
class UserToolAssociation(db.Model):
__tablename__ = 'user_tool_association'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
tool_id = db.Column(db.Integer, db.ForeignKey('tool.id'))
property1_specific_to_this_user_tool = db.Column(db.String(20))
property2_specific_to_this_user_tool = db.Column(db.String(20))
user = db.relationship("User")
tool = db.relationship("Tool")
由于适当分离关注点,上述方法更好。将来当您需要做一些会影响所有锤子的事情时,您只需在“工具”表中修改锤子实例即可。如果将所有锤子作为单独的实例放置而在它们之间没有任何链接,那么将来对它们整体进行任何修改将变得很麻烦。