我正在使用SQLAlchemy与SQL Server数据库进行交互。
数据库中的一个表具有一个主键,该主键也是一个外键(建模可选的一对一关系)。尝试使用SQLAlchemy ORM插入此表会产生意外错误; SQLAlchemy似乎试图在外表中创建新行,然后将其ID用作外键/主键列的值-完全忽略该列的显式指定值。
具体来说,我的数据库架构的相关部分如下:
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)
尝试使用ORM ScalarData
样式插入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
的{{1}}的值如何,都会发生这种情况。
我尝试在ScalarData
模型定义的autoincrement=False
调用中添加DataSetId = Column(...)
,但是行为完全不变。
在这一点上我很沮丧。对于如何解决此问题,甚至只是为什么会发生这种问题的任何见解,都是很棒的。
答案 0 :(得分:0)
我知道这有点晚了,但是为什么ScalarData继承自DataSet?如果直接来自sqlacodegen,那我认为是一个错误。