Если вы хотите смоделировать домен, позволяя именам инструментов быть неуникальными, то сделать это нелегко.
Вы можете попробовать добавить валидатор в модель 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
Это также вызывает исключение. Поэтому на уровне БД мы убедились, что ваше требование выполнено.
Но, на мой взгляд, такой сложный подход обычно означает, что мы без необходимости усложняем дизайн. Если вы согласны с изменением дизайна таблицы, рассмотрите следующее предложение.для более простого подхода.
По моему мнению, лучше иметь набор уникальных инструментов и набор уникальных пользователей, а затем моделировать отношения M2M между ними.Любое свойство, характерное для молотка Майка, но не присутствующее в молотке Джеймса, должно быть свойством этой ассоциации между ними.
Если вы выберете такой подход, у вас будет набор таких пользователей, как этот
Майк, Джеймс, Джон, Джордж
и набор подобных инструментов
Молоток, отвертка, клин, ножницы, нож
И вы все еще можете смоделировать отношения между многими.В этом сценарии единственное изменение, которое вам нужно сделать, это установить unique=True
в столбце Tool.name
, чтобы в мире был только один молоток, который может иметь это имя.
Если вам нужен молоток Майка дляиметь некоторые уникальные свойства, отличные от 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")
Приведенный выше подход лучше из-за правильного разделения интересов.В будущем, когда вам нужно будет сделать что-то, что повлияет на все молотки, вы можете просто изменить экземпляр молотка в таблице Tools.Если вы будете хранить все молотки как отдельные экземпляры без какой-либо связи между ними, то в будущем будет сложнее вносить какие-либо изменения в них в целом.