Ответ 1
Основная проблема в этом случае - это не только отношения "многие ко многим", но и то, что это самореферентное отношение "многие ко многим". Поскольку automap
просто переводит сопоставленные имена классов в имена отношений, он task_collection
одно и то же имя, например task_collection
, для обоих направлений отношения, а столкновение имен порождает ошибку. Этот недостаток automap
чувствует себя значимым в том, что самореференциальные, многие-ко-многим отношения не являются редкостью.
Явное добавление отношений, которые вы хотите, используя свои собственные имена, не решит проблему, потому что automap
все равно попытается создать отношения task_collection
. Чтобы справиться с этой проблемой, нам необходимо переопределить task_collection
.
Если вы поддерживаете имя task_collection
для прямого направления отношений, мы можем просто предварительно определить взаимосвязь - указать любое имя, которое мы хотим для backref
. Если automap
находит ожидаемое свойство уже на месте, он предположит, что отношение переопределено и не пытается его добавить.
Здесь приведенный пример, а также база данных sqlite
для тестирования.
База данных Sqlite
CREATE TABLE task (
id INTEGER,
name VARCHAR,
PRIMARY KEY (id)
);
CREATE TABLE task_task (
tid1 INTEGER,
tid2 INTEGER,
FOREIGN KEY(tid1) REFERENCES task(id),
FOREIGN KEY(tid2) REFERENCES task(id)
);
-- Some sample data
INSERT INTO task VALUES (0, 'task_0');
INSERT INTO task VALUES (1, 'task_1');
INSERT INTO task VALUES (2, 'task_2');
INSERT INTO task VALUES (3, 'task_3');
INSERT INTO task VALUES (4, 'task_4');
INSERT INTO task_task VALUES (0, 1);
INSERT INTO task_task VALUES (0, 2);
INSERT INTO task_task VALUES (2, 4);
INSERT INTO task_task VALUES (3, 4);
INSERT INTO task_task VALUES (3, 0);
Вводя его в файл setup_self.sql
, мы можем сделать:
sqlite3 self.db < setup_self.sql
Код Python
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
DeclBase = declarative_base()
task_task = Table('task_task', DeclBase.metadata,
Column('tid1', Integer, ForeignKey('task.id')),
Column('tid2', Integer, ForeignKey('task.id')))
Base = automap_base(DeclBase)
class Task(Base):
__tablename__ = 'task'
task_collection = relationship('Task',
secondary=task_task,
primaryjoin='Task.id==task_task.c.tid1',
secondaryjoin='Task.id==task_task.c.tid2',
backref='backward')
engine = create_engine("sqlite:///self.db")
Base.prepare(engine, reflect=True)
session = Session(engine)
task_0 = session.query(Task).filter_by(name ='task_0').first()
task_4 = session.query(Task).filter_by(name ='task_4').first()
print("task_0.task_collection = {}".format([x.name for x in task_0.task_collection]))
print("task_4.backward = {}".format([x.name for x in task_4.backward]))
Результаты
task_0.task_collection = ['task_1', 'task_2']
task_4.backward = ['task_2', 'task_3']
Использование другого имени
Если вы хотите иметь имя, отличное от task_collection
, вам нужно использовать функцию automap
для переопределения имен связей коллекции:
name_for_collection_relationship(base, local_cls, referred_cls, constraint)
Аргументы local_cls
и referred_cls
являются экземплярами отображенных классов таблиц. Для отношений с самореференцией, "многие-ко-многим" они являются одинаковыми. Мы можем использовать аргументы для построения ключа, который позволяет идентифицировать переопределения.
Вот пример реализации этого подхода.
from sqlalchemy.ext.automap import automap_base, name_for_collection_relationship
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
DeclBase = declarative_base()
task_task = Table('task_task', DeclBase.metadata,
Column('tid1', Integer, ForeignKey('task.id')),
Column('tid2', Integer, ForeignKey('task.id')))
Base = automap_base(DeclBase)
class Task(Base):
__tablename__ = 'task'
forward = relationship('Task',
secondary=task_task,
primaryjoin='Task.id==task_task.c.tid1',
secondaryjoin='Task.id==task_task.c.tid2',
backref='backward')
# A dictionary that maps relationship keys to a method name
OVERRIDES = {
'Task_Task' : 'forward'
}
def _name_for_collection_relationship(base, local_cls, referred_cls, constraint):
# Build the key
key = '{}_{}'.format(local_cls.__name__, referred_cls.__name__)
# Did we have an override name?
if key in OVERRIDES:
# Yes, return it
return OVERRIDES[key]
# Default to the standard automap function
return name_for_collection_relationship(base, local_cls, referred_cls, constraint)
engine = create_engine("sqlite:///self.db")
Base.prepare(engine, reflect=True, name_for_collection_relationship=_name_for_collection_relationship)
Обратите внимание, что переопределение name_for_collection_relationship
просто изменяет имя, которое automap
использует для отношения. В нашем случае связь по-прежнему определяется Task
. Но переопределение говорит automap
искать forward
а не task_collection
, который он находит и, следовательно, прекращает определять отношения.
Другие подходы
В некоторых случаях было бы неплохо, если бы мы могли переопределить имена отношений, не предварительно определяя фактические отношения. При первом рассмотрении это должно быть возможно с помощью name_for_collection_relationship
. Однако я не мог заставить этот подход работать для самореферентных отношений "многие ко многим" из-за сочетания двух причин.
-
name_for_collection_relationship
и связанное с нимgenerate_relationship
дважды, один раз для каждого направления отношений "многие ко многим". В обоих случаях,local_cls
иreferred_cls
такие же, из - за собственной референциальности. Более того, другие аргументыname_for_collection_relationship
фактически эквивалентны. Поэтому мы не можем, из контекста вызова функции, определить, какое направление мы переопределяем. -
Вот еще более удивительная часть проблемы. Кажется, мы не можем даже рассчитывать на одно направление, происходящее перед другим. Другими словами, два вызова
name_for_collection_relationship
иgenerate_relationship
очень похожи. Аргументом, который фактически определяет направленность отношения, являетсяconstraint
, которое является одним из двух ограничений внешнего ключа для отношения; эти ограничения загружаются изBase.metadata
в переменную, называемуюm2m_const
. В этом и заключается проблема. Порядок, в котором ограничения заканчиваются вm2m_const
является недетерминированным, т.m2m_const
Иногда это будет один порядок; в других случаях это будет наоборот (по крайней мере, при использованииsqlite3
). Из-за этого направленность отношения является недетерминированной.
С другой стороны, когда мы предварительно определяем взаимосвязь, следующие аргументы создают необходимый детерминизм.
primaryjoin='Task.id==task_task.c.tid1',
secondaryjoin='Task.id==task_task.c.tid2',
Особо следует отметить, что я на самом деле пытался создать решение, которое просто переопределяло имена отношений без предварительного определения. Он показал описанный недетерминизм.
Последние мысли
Если у вас есть разумное количество таблиц базы данных, которые часто не меняются, я бы предложил просто использовать Declarative Base. Это может быть немного больше работы по настройке, но это дает вам больше контроля.