Ошибка SQLAlchemy при вставке в таблицу, первичный ключ которой также является внешним ключом - PullRequest
0 голосов
/ 26 апреля 2019

Я использую SQLAlchemy для взаимодействия с базой данных SQL Server.

Одна из таблиц в моей базе данных имеет первичный ключ, который также является внешним ключом (моделирует необязательное отношение «один к одному»). Попытка вставить в эту таблицу с помощью SQLAlchemy ORM приводит к непредвиденной ошибке; Похоже, что SQLAlchemy пытается создать новую строку во внешней таблице, идентификатор которой затем используется в качестве значения для столбца внешнего / первичного ключа - полностью игнорируя явно указанное значение для этого столбца.

Если говорить конкретно, то соответствующая часть моей схемы БД выглядит так:

CREATE TABLE [dbo].[DataType] (
  [Id] INT IDENTITY (1,1) NOT NULL,
  [Name] NVARCHAR(200) NOT NULL UNIQUE,
  [DataTable] NVARCHAR(50) NOT NULL,
  CONSTRAINT [PK_Type] PRIMARY KEY CLUSTERED ([Id] ASC),
)

CREATE TABLE [dbo].[DataSet] (
  [Id] INT IDENTITY (1,1) NOT NULL,
  [DataTypeId] INT NOT NULL,
  CONSTRAINT [PK_DataSet] PRIMARY KEY CLUSTERED ([Id] ASC),
  CONSTRAINT [FK_DataSet_DataType] FOREIGN KEY ([DataTypeId]) REFERENCES [dbo].[Type] ([Id]),
)

CREATE TABLE [dbo].[ScalarData] (
  [DataSetId] INT NOT NULL,
  [Value] VARBINARY(MAX) NOT NULL,
  CONSTRAINT [PK_ScalarData] PRIMARY KEY CLUSTERED ([DataSetId] ASC),
  CONSTRAINT [FK_ScalarData_DataSet] FOREIGN KEY ([DataSetId]) REFERENCES [dbo].[DataSet] ([Id]) ON DELETE CASCADE,
)

Я использовал инструмент sqlacodegen, чтобы автоматически сгенерировать соответствующий код модели SQLAlchemy, получив такой вывод:

from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Unicode
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
metadata = Base.metadata


class DataType(Base):
    __tablename__ = "DataType"

    Id = Column(Integer, primary_key=True)
    Name = Column(Unicode(200), nullable=False, unique=True)


class DataSet(Base):
    __tablename__ = "DataSet"

    Id = Column(Integer, primary_key=True)
    TypeId = Column(ForeignKey("DataType.Id"), nullable=False)

    DataType = relationship("DataType")


class ScalarData(DataSet):
    __tablename__ = "ScalarData"

    DataSetId = Column(ForeignKey("DataSet.Id"), primary_key=True)
    Value = Column(LargeBinary, nullable=False)

Проблема возникает при попытке вставить в ScalarData в стиле ORM session.add(). Похоже, что при добавлении объекта ScalarData SQLAlchemy всегда пытается создать новый объект DataSet для ссылки в DataSetId - но это не удается, потому что новый объект DataSet предоставляет нулевое значение для DataTypeId, которое не может быть обнулено.

Желаемое поведение заключается в том, что я могу явно создать DataSet, а затем передать его Id в качестве значения для DataSetId при создании нового объекта ScalarData - но когда я это делаю, кажется, что переданное значение для DataSetId полностью игнорируется, и SQLAlchemy все еще пытается создать новый DataSet.

Странно, но проблема не возникает, если я вставляю новый ScalarData, используя session.execute().

Ниже приведен минимальный пример воспроизведения ошибки, начиная с пустой базы данных с вышеуказанной схемой:

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

engine = create_engine(
    "mssql+pyodbc://username:password@localhost/my_database?driver=ODBC Driver 17 for SQL Server"
)

session = Session(bind=engine)

datatype = DataType(Name="foo")
session.add(datatype)
session.flush()

dataset1 = DataSet(TypeId=datatype.Id)
session.add(dataset1)
session.flush()

dataset2 = DataSet(TypeId=type.Id)
session.add(dataset2)
session.flush()

session.execute(
    ScalarData.__table__.insert().values(DataSetId=dataset1.Id, Value=b"123")
)
session.flush() # this goes through just fine

data = ScalarData(DataSetId=dataset2.Id, Value=b"123")
session.add(data)
session.flush() # error raised here

Возникло следующее исключение:

sqlalchemy.exc.IntegrityError: (pyodbc.IntegrityError) ('23000', "[23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot insert the value NULL into column 'DataTypeId', table 'my_database.dbo.DataSet'; column does not allow nulls. INSERT fails. (515) (SQLExecDirectW)")
[SQL: INSERT INTO [DataSet] ([DataTypeId]) OUTPUT inserted.[Id] VALUES (?)]
[parameters: (None,)]

Я попытался отключить ограничение NOT NULL для DataTypeId, просто чтобы посмотреть, что произойдет, когда запрос выполнит все, что он пытается сделать. В этом случае SQL будет выглядеть следующим образом:

INSERT INTO [DataSet] ([DataTypeId]) OUTPUT inserted.[Id] VALUES (?)
(None,)
INSERT INTO [ScalarData] ([DataSetId], [Value]) VALUES (?, ?)
(27, bytearray(b'123'))

Приведенное выше значение 27 действительно является значением Id только что созданной строки DataSet (конечно, это зависит от каждого вызова). Это происходит независимо от значения DataSetId, переданного в ScalarData.

Я попытался добавить autoincrement=False к вызову DataSetId = Column(...) в определении модели ScalarData, но поведение полностью не изменилось.

Я довольно сильно озадачен. Любое понимание того, как решить эту проблему, или даже просто почему она происходит, было бы замечательно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...