【发布时间】:2019-09-15 20:26:42
【问题描述】:
我正在使用 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 session.add() 样式插入 ScalarData 时会出现问题。看来,当添加 ScalarData 对象时,SQLAlchemy always 尝试创建一个新的 DataSet 对象以在 DataSetId 中引用 - 但这失败了,因为新的 DataSet 对象提供DataTypeId 的空值,不可为空。
所需的行为是我可以显式创建一个DataSet,然后将其Id 作为DataSetId 的值传递给创建一个新的ScalarData 对象-但是当我这样做时,似乎DataSetId 的传入值被完全忽略,SQLAlchemy 仍然尝试创建一个新的 DataSet。
奇怪的是,如果我使用session.execute() 插入新的ScalarData,似乎不会出现问题。
以下是重现错误的最小示例,从具有上述架构的空数据库开始:
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,)]
我尝试禁用DataTypeId 上的NOT NULL 约束,只是为了看看当查询成功时会发生什么。在这种情况下,生成的 SQL 如下:
INSERT INTO [DataSet] ([DataTypeId]) OUTPUT inserted.[Id] VALUES (?)
(None,)
INSERT INTO [ScalarData] ([DataSetId], [Value]) VALUES (?, ?)
(27, bytearray(b'123'))
上面的值 27 确实是新创建的DataSet 行的Id 值(当然,这会随着每次调用而变化)。无论传递给ScalarData 的DataSetId 的值如何,都会发生这种情况。
我尝试将autoincrement=False 添加到ScalarData 模型定义中的DataSetId = Column(...) 调用,但行为完全没有改变。
在这一点上我非常难过。任何关于如何解决这个问题的见解,甚至是为什么会发生,都会很棒。
【问题讨论】:
标签: python sql-server orm sqlalchemy