SQLalchemy устанавливает ограничения на отношения во многих ко многим - PullRequest
0 голосов
/ 28 сентября 2018

Предположим, у меня есть набор пользователей, и у каждого пользователя есть доступ к коллекции инструментов.У одного и того же инструмента может быть много пользователей с доступом, поэтому это отношение многие ко многим:

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.
  • A Tool, существующий в базе данных, не может быть добавлен в коллекцию tools пользователя, еслиодин с таким именем уже существует в наборе, то есть Hammer Джона1 не может быть предоставлен Mike1, так как у Mike1 уже есть свой Hammer.
  • James, однако, он может создать новый Hammerтак как у него уже нет молотка.Тогда в базе данных будет 3 инструмента с именем Hammer, каждый с отдельным набором Users.
  • Обратите внимание, что в моем конкретном случае Tool будет существовать, только если в нем есть хотя бы один User, но я также не знаю, как обеспечить это изначально в моей базе данных.

Возможно ли это изначально с помощью SQLalchemy для автоматической настройки моей базы данных для поддержания целостности?Я не хочу писать свои собственные правила валидатора, так как, скорее всего, что-то упущу и получу базу данных, которая нарушает мои правила.

Ответы [ 2 ]

0 голосов
/ 28 сентября 2018

Если вы хотите смоделировать домен, позволяя именам инструментов быть неуникальными, то сделать это нелегко.

Вы можете попробовать добавить валидатор в модель 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.Если вы будете хранить все молотки как отдельные экземпляры без какой-либо связи между ними, то в будущем будет сложнее вносить какие-либо изменения в них в целом.

0 голосов
/ 28 сентября 2018

Проблема заключается в том, как выразить предикат «У пользователя, идентифицированного по 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()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...