插入主键也是外键的表中的SQLAlchemy错误

时间:2019-04-26 10:27:22

标签: python sql-server orm sqlalchemy

我正在使用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(...),但是行为完全不变。

在这一点上我很沮丧。对于如何解决此问题,甚至只是为什么会发生这种问题的任何见解,都是很棒的。

1 个答案:

答案 0 :(得分:0)

我知道这有点晚了,但是为什么ScalarData继承自DataSet?如果直接来自sqlacodegen,那我认为是一个错误。