#python #many-to-many #sqlalchemy #associations
#python #многие ко многим #sqlalchemy #ассоциации
Вопрос:
Я прочитал документацию по SQLAlchemy и руководство по построению отношения «многие ко многим», но я не мог понять, как это сделать правильно, когда таблица ассоциаций содержит более двух внешних ключей.
У меня есть таблица элементов, и каждый элемент содержит множество деталей. Детали могут быть одинаковыми для многих элементов, поэтому между элементами и деталями существует отношение «многие ко многим»
У меня есть следующее:
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
Моя таблица ассоциаций (она определена перед двумя другими в коде):
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
В документации сказано, что мне нужно использовать «объект ассоциации». Я не мог понять, как правильно его использовать, поскольку он смешан декларативно с формами mapper, и примеры кажутся неполными. Я добавил строку:
details = relation(ItemDetail)
как член класса Item и строка:
itemDetail = relation('Detail')
в качестве члена таблицы ассоциаций, как описано в документации.
когда я делаю item = session.запрос (Item).first(), item.details — это не список объектов Detail, а список объектов ItemDetail.
Как я могу правильно получить сведения в объектах Item, т. е. item.details должен быть списком объектов Detail?
Комментарии:
1. sqlalchemy.org/docs/orm/extensions/associationproxy.html очень подробный и содержит множество примеров. Не уверен, что здесь еще нужно.
2. Прокси-сервер ассоциации был не тем, что я искал. Я заменил объявление класса ItemDetial табличным и использовал вторичный параметр в функции relation
Ответ №1:
Из комментариев я вижу, что вы нашли ответ. Но документация по SQLAlchemy довольно сложна для «нового пользователя», и я боролся с тем же вопросом. Итак, для дальнейшего использования:
ItemDetail = Table('ItemDetail',
Column('id', Integer, primary_key=True),
Column('itemId', Integer, ForeignKey('Item.id')),
Column('detailId', Integer, ForeignKey('Detail.id')),
Column('endDate', Date))
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail, backref='Detail')
Комментарии:
1. Не уверен почему, но в моем случае мне пришлось добавить
Base.metadata
вItemDetail
таблицу, как описано здесь .2. @Kerma У меня другая проблема. как поместить отношение в один класс вместо того, чтобы помещать его в класс Item и класс Detail отдельно??
3. В моем случае мне пришлось фактически удалить
Base.metadata
, чтобы удалить «FK, связанный со столбцом …»4. ммм, так как вы создаете новые детали и связываете их с нужными элементами?
5. Точно так же, как это делалось в обычных базах данных
Ответ №2:
Как и Мигель, я также использую декларативный подход для моей таблицы соединений. Однако я продолжал сталкиваться с ошибками, такими как
sqlalchemy.exc.ArgumentError: вторичный аргумент <класс’, основной.ProjectUser’> передано пользователю to relationship() .проекты должны быть объектом Table или другим предложением FROM; не может напрямую отправлять сопоставленный класс, поскольку строки во ‘secondary’ сохраняются независимо от класса, который сопоставлен с этой же таблицей.
Немного повозившись, я смог придумать следующее. (Обратите внимание, что мои классы отличаются от OP, но концепция та же.)
Пример
Вот полный рабочий пример
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, Session
# Make the engine
engine = create_engine("sqlite pysqlite:///:memory:", future=True, echo=False)
# Make the DeclarativeMeta
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
projects = relationship('Project', secondary='project_users', back_populates='users')
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary='project_users', back_populates='projects')
class ProjectUser(Base):
__tablename__ = "project_users"
id = Column(Integer, primary_key=True)
notes = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
# Create the tables in the database
Base.metadata.create_all(engine)
# Test it
with Session(bind=engine) as session:
# add users
usr1 = User(name="bob")
session.add(usr1)
usr2 = User(name="alice")
session.add(usr2)
session.commit()
# add projects
prj1 = Project(name="Project 1")
session.add(prj1)
prj2 = Project(name="Project 2")
session.add(prj2)
session.commit()
# map users to projects
prj1.users = [usr1, usr2]
prj2.users = [usr2]
session.commit()
with Session(bind=engine) as session:
print(session.query(User).where(User.id == 1).one().projects)
print(session.query(Project).where(Project.id == 1).one().users)
Примечания
- ссылайтесь на имя таблицы в
secondary
аргументе likesecondary='project_users'
в отличие отsecondary=ProjectUser
- используйте
back_populates
вместоbackref
Я сделал подробную запись об этом здесь.
Комментарии:
1. Вы пробовали:
secondary=ItemDetail.__table__
илиsecondary=ItemDetail.__tablename__
2. Неполный ответ выдает ошибку
3. @AdnanFayaz Пожалуйста, предоставьте более подробную информацию — какая ошибка выдается и какую версию Python и SQLA вы используете? У меня это работает на Py 3.110.rc1 с SQLA 2.0.2
Ответ №3:
Предыдущий ответ сработал для меня, но я использовал подход на основе классов для таблицы ItemDetail. Это пример кода:
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True, index=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail.__table__, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail.__table__, backref='Detail')
Комментарии:
1. Используя этот подход, я получаю следующую ошибку:
sqlalchemy.exc.ArgumentError: secondary argument <class> passed to to relationship() Activity.participants must be a Table object or other FROM clause; can't send a mapped class directly as rows in 'secondary' are persisted independently of a class that is mapped to that same table.
что наводит меня на мысль, что сопоставленные классы нельзя использовать в качестве вторичного атрибута2. Это должно было бы быть
secondary=ItemDetail.__table__