Я использую 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
, но поведение полностью не изменилось.
Я довольно сильно озадачен. Любое понимание того, как решить эту проблему, или даже просто почему она происходит, было бы замечательно.