Как построить отношения «многие ко многим» с помощью SQLAlchemy: хороший пример

#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)
  

Примечания

  1. ссылайтесь на имя таблицы в secondary аргументе like secondary='project_users' в отличие от secondary=ProjectUser
  2. используйте 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__