如果您想通过允许工具名称不唯一来对域进行建模,那么就没有简单的方法来实现这一点。
您可以尝试向 User 模型添加一个验证器,该验证器将在每次追加期间检查 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 必须是唯一的组合
这里是代码
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
这也会引发异常。因此,我们已确保在 db 级别解决了您的要求。
但在我看来,采用这种复杂的方法通常表明我们不必要地使设计复杂化。如果您可以更改表格设计,请考虑以下建议以获得更简单的方法。
在我看来,最好拥有一组独特的工具和一组独特的用户,然后对它们之间的 M2M 关系进行建模。任何特定于 Mike 的锤子但不存在于 James 的锤子中的属性都应该是它们之间关联的属性。
如果你采用这种方法,你就会拥有一组这样的用户
迈克、詹姆斯、约翰、乔治
还有一套这样的工具
锤子、螺丝刀、楔子、剪刀、刀
您仍然可以对它们之间的多对多关系进行建模。在这种情况下,您唯一需要做的更改是在Tool.name 列上设置unique=True,以便全局只有一个锤子可以使用该名称。
如果您需要 Mike 的锤子具有与 James 的锤子不同的一些独特属性,那么您可以在关联表中添加一些额外的列。要访问 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")
由于适当的关注点分离,上述方法更好。以后当你需要做一些会影响所有锤子的事情时,你可以在工具表中修改锤子实例。如果您将所有锤子作为单独的实例,它们之间没有任何链接,那么将来对它们进行任何整体修改都会变得很麻烦。