如果您想通过允许工具名称不唯一来建模域,则没有简单的方法可以实现此目标。
您可以尝试向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
共同行为为
组合外键
(请参阅
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
这也会抛出异常。因此,我们已经在数据库层面上确保了您的需求得到解决。
但是,在我看来,采用这种复杂的方法通常意味着我们在不必要地复杂化设计。如果您可以接受更改表格设计,请考虑以下建议以获得更简单的方法。
在我看来,最好拥有一组独特的工具和一组独特的用户,然后在它们之间建立一个M2M关系。任何仅适用于Mike的锤子而James的锤子中不存在的属性都应该是它们之间关联的属性。
如果您采取这种方法,您将拥有以下用户集合
Mike,James,John,George
和以下工具集合
锤子,螺丝刀,楔子,剪刀,刀子
并且您仍然可以在它们之间建立多对多的关系。在这种情况下,您唯一需要做的更改是在Tool.name
列上设置unique=True
,以便全局只有一个名为“锤子”的工具。
如果您需要让Mike的锤子具有与James的锤子不同的一些独特属性,那么您只需在关联表中添加一些额外的列。要访问用户.工具和工具.用户,您可以使用一个关联代理。
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")
以上方法更好,因为它合理地分离了关注点。将来,如果您需要做一些会影响所有锤子的事情,您只需在工具表中修改锤子实例即可。如果您将所有锤子保持为单独的实例而没有任何联系,那么将来要对它们进行任何整体修改都会变得麻烦。