sqlalchemy объединяет себя с промежуточной таблицей (работает sql, а не sqlalchemy) - PullRequest
0 голосов
/ 10 марта 2020

Я пытаюсь выполнить соединение «многие ко многим» с промежуточной таблицей. (сначала выберите строку (и) в Items, присоединитесь к Attribution на FK, затем присоединитесь к другому FK в атрибуции, чтобы получить больше от Items) Схема выглядит следующим образом:

class Items(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True)
    name = Column(Text)
    project = Column(Integer)
    linkid = Column(Integer, ForeignKey("items.linkid"))

    linked = relationship("Items", foreign_keys="[Items.linkid]")

class Attribution(Base):
    __tablename__ = "attribution"

    id = Column(Integer, primary_key=True)
    link_id_d = Column(Integer, ForeignKey('items.linkid'))
    link_id_m = Column(Integer, ForeignKey('items.linkid'))

И запрос выглядит так :

final_items = aliased(Items)
proj_1 = session.query(Items)\
    .join(Attribution, ( (Items.name=="upper_third") & (Attribution.link_id_m==Items.linkid) ))\
    .join(final_items, final_items.linkid==Attribution.link_id_d)\
    .all()

Это обеспечивает только одну строку; вывод:

upper_third 1

sql, который я использую через sqlite, который предоставляет три ожидаемые строки, выглядит так:

SELECT * FROM items
join attribution on items.name = 'upper_third' and items.linkid == attribution.link_id_m
join items as tbl1 on tbl1.linkid == attribution.link_id_d

Вывод:

5|upper_third|1|1|1|2|1|2|lower_first|2|2
5|upper_third|1|1|1|2|1|4|lower_second|2|2
5|upper_third|1|1|1|2|1|6|lower_third|2|2

В чем логическое различие между этими двумя запросами и как я могу перенести решение sql в sqalchemy?

(полный исполняемый код для создания базы данных sqlite и тестовых запросов приведен ниже)

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, UniqueConstraint, ForeignKey, func, and_
from sqlalchemy.orm import sessionmaker, relationship, aliased, backref

Base = declarative_base()

from sqlalchemy import Column, Integer, String, Float, Boolean, Text, Table

class Items(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True)
    name = Column(Text)
    project = Column(Integer)
    linkid = Column(Integer, ForeignKey("items.linkid"))

    linked = relationship("Items", foreign_keys="[Items.linkid]")
    attributions_m = relationship("Attribution", foreign_keys="[Attribution.link_id_m]")
    attributions_d = relationship("Attribution", foreign_keys="[Attribution.link_id_d]")

class Attribution(Base):
    __tablename__ = "attribution"

    id = Column(Integer, primary_key=True)
    link_id_d = Column(Integer, ForeignKey('items.linkid'))  # one of the tests to link to
    link_id_m = Column(Integer, ForeignKey('items.linkid'))  # one of the tests to link to


import os


if os.path.exists('app.db'):
    os.remove('app.db')

engine = create_engine('sqlite+pysqlite:///app.db')

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()


u1 = Items(name="upper_first", linkid=1, project=1)
l1 = Items(name="lower_first", linkid=2, project=2)

u2 = Items(name="upper_second", linkid=1, project=1)
l2 = Items(name="lower_second", linkid=2, project=2)

u3 = Items(name="upper_third", linkid=1, project=1)
l3 = Items(name="lower_third", linkid=2, project=2)

session.add(u1)
session.add(l1)
session.add(u2)
session.add(l2)
session.add(u3)
session.add(l3)

session.commit()

a1 = Attribution(link_id_m=u3.linkid, link_id_d=l3.linkid)
session.add(a1)
session.commit()

final_items = aliased(Items)
proj = session.query(Items)\
    .join(Attribution, ( (Items.name=="upper_third") & (Attribution.link_id_m==Items.linkid) ))\
    .join(final_items, final_items.linkid==Attribution.link_id_d)\
    .all()

for l in proj:
    print(l.name, l.linkid)

1 Ответ

1 голос
/ 10 марта 2020

Краткий ответ: SQLAlchemy работает должным образом.

Ключевым виновником является тот факт, что при запросе сопоставленной модели вы получите экземпляры модели в виде результат. Если одна и та же модель возвращается несколько раз, SA будет возвращать каждую только один раз, и поэтому у вас возвращается только 1 строка вместо ожидаемых 3.

Это можно увидеть по разнице в SQL, который вы создаете (select * ...) против того, который создает для вас SA (select items.*; SA не создает *, но дело в том, что он выбирает только из таблицы items.

Решение состоит в том, чтобы добавить и другие сущности к query(...), которые должны быть возвращены:

final_items = aliased(Items, name="FinalItems")
proj = (
    session
    .query(Items, Attribution, final_items)  # IMPORTANT !!!
    .join(Attribution, ( (Items.name=="upper_third") & (Attribution.link_id_m==Items.linkid) ))
    .join(final_items, final_items.linkid==Attribution.link_id_d)
)

Выполнение, как показано ниже:

for l in proj.all():
    print(l)
    # print(l.Items, l.Attribution, l.FinalItems)  # also can access models using names.

... приведет к список tuple(Items, Attribution, Items):

(<Items(id=5, linkid=1, name='upper_third', project=1)>, <Attribution(id=1, link_id_d=2, link_id_m=1)>, <Items(id=2, linkid=2, name='lower_first', project=2)>)
(<Items(id=5, linkid=1, name='upper_third', project=1)>, <Attribution(id=1, link_id_d=2, link_id_m=1)>, <Items(id=4, linkid=2, name='lower_second', project=2)>)
(<Items(id=5, linkid=1, name='upper_third', project=1)>, <Attribution(id=1, link_id_d=2, link_id_m=1)>, <Items(id=6, linkid=2, name='lower_third', project=2)>)

Если вы действительно хотите, чтобы столбцы были возвращены, как в вашем примере, вы можете выполнить statement запроса. Код ниже

for row in session.execute(proj.statement):
    print(row)

вернет:

(5, 'upper_third', 1, 1, 1, 2, 1, 2, 'lower_first', 2, 2)
(5, 'upper_third', 1, 1, 1, 2, 1, 4, 'lower_second', 2, 2)
(5, 'upper_third', 1, 1, 1, 2, 1, 6, 'lower_third', 2, 2)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...