附加到关系时,SQLAlchemy异常不为null失败

时间:2019-05-30 13:01:43

标签: python sqlalchemy

SQLAlchemy出现一个非常奇怪的错误。我已删除了尽可能多的代码以缩小问题的范围,删除更多代码将导致错误消失。通过重新安装SQLAlchemy(在Python 2.7上),我可以在另一台PC上重现该问题。

如果我做任何追加这样的事情的变体:

python = Application(name='Python')
python.versions.append(ApplicationVersion(version=27))
session.add(python)
session.commit()

#or

python = Application(name='Python')
session.add(python)
session.commit()
python.versions.append(ApplicationVersion(version=27))
session.commit()

我收到此错误消息(如果我没有从代码中删除其他任何内容):

sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) NOT NULL constraint failed: ApplicationVersion.application_id
[SQL: INSERT INTO "ApplicationVersion" (application_id, version_int) VALUES (?, ?)]
[parameters: (None, 27)]

但是,就像我提到的,如果我删除任何东西,它就可以完美地工作。例如,通过删除以下函数的文档字符串,它将正确分配application_id并按预期工作。

@contextmanager
def Session():
    """Setup session to allow for usage with a context manager."""
    session = _Session()
    yield session
    session.close()

我真的不知道发生了什么。作为免责声明,我测试的另一台PC在相同的工作网络上,但是由于我正在使用sqlite进行测试,因此我无法想象它是基于网络的。

以下是重现该错误的代码(这是一个由多个文件合并而成的文件):

######### CONNECT.PY #######
import os
from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import sessionmaker


class BaseTable(object):
    """General things to apply to each table.

    Help: https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/mixins.html
    """
    @declared_attr
    def __tablename__(cls):
        """Set the table name to that of the model."""
        return cls.__name__


if 'DATABASE_URL' not in os.environ:
    os.environ['DATABASE_URL'] = 'sqlite://'

Engine = create_engine(os.environ['DATABASE_URL'])

Base = declarative_base(bind=Engine, cls=BaseTable)

_Session = sessionmaker(bind=Base.metadata.bind)


@contextmanager
def Session():
    """Setup session to allow for usage with a context manager."""
    session = _Session()
    yield session
    session.close()


########## MODELS.PY ###########
import time
import os
from sqlalchemy import Column, Integer, SmallInteger, String, Text
from sqlalchemy import ForeignKey, UniqueConstraint, Table, event
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import backref, relationship, validates


class Category(Base):
    row_id = Column(Integer, primary_key=True)
    name = Column(String(64), nullable=False)
    parent_id = Column(Integer, ForeignKey('Category.row_id'), nullable=True)

    parent = relationship('Category', foreign_keys=parent_id, remote_side=row_id)
    children = relationship('Category')

    __table_args__ = (
        UniqueConstraint('name', 'parent_id', name='unique_name_parent'),
    )

    @hybrid_property
    def fullname(self):
        parent = self.parent
        visited = set()
        chain = [self.name]
        while parent:
            if parent in visited:
                break
            visited.add(parent)
            chain.append(parent.name)
            parent = parent.parent

        return '.'.join(chain[::-1])

    def __init__(self, name, collection, parent=None, creator=None, **kwargs):
        super(Category, self).__init__(name=name, collection=collection, parent=parent, creator=creator, **kwargs)

    def __repr__(self):
        return '<{cls} "{fullname}">'.format(
            cls=self.__class__.__name__,
            fullname=self.fullname,
        )


class Application(Base):
    row_id = Column(Integer, primary_key=True)
    name = Column(String(16), nullable=False)
    versions = relationship('ApplicationVersion', order_by='ApplicationVersion.version_int')


class ApplicationVersion(Base):
    row_id = Column(Integer, primary_key=True)
    application_id = Column(Integer, ForeignKey('Application.row_id'), nullable=False)
    version_int = Column(Integer, nullable=False)

    application = relationship('Application', foreign_keys=application_id)

    __table_args__ = (
        UniqueConstraint('application_id', 'version_int', name='unique_application_version'),
    )

    def __init__(self, version, application=None, **kwargs):
        super(ApplicationVersion, self).__init__(application=application, version_int=version, **kwargs)

    def __repr__(self):
        return '<{cls} "{application} {version}">'.format(
            cls=self.__class__.__name__,
            application=self.application.name,
            version=self.version_int,
        )

    def __eq__(self, num):
        return self.version_int == num

    def __neq__(self, num):
        return self.version_int != num


######## TEST.PY ########
Base.metadata.create_all()

if __name__ == '__main__':
    with Session() as session:

        # Setup programs and versions
        python = Application(name='Python')
        python.versions.append(ApplicationVersion(version=27))
        session.add(python)
        session.commit()

        print python.versions

这些可以阻止错误的动作如下:

  • BaseTableSession删除文档字符串
  • 删除if 'DATABASE_URL' not in os.environ:
  • create_engine(os.environ['DATABASE_URL'])替换为 create_engine('sqlite://')
  • 删除Category
  • Category删除关系
  • fullname中删除__init____repr__Category
  • 从中删除__init____repr____eq____neq__ ApplicationVersion

任何帮助将不胜感激,这使我有些疯狂。我可以使用session.add(ApplicationVersion(python, 27))来解决这个问题,但是我想知道这里到底发生了什么,因为我以前从未见过Python如此表现。

1 个答案:

答案 0 :(得分:2)

我发现问题出在您在ApplicationVersion上定义的自定义构造函数中:

def __init__(self, version, application=None, **kwargs):
    super(ApplicationVersion, self).__init__(application=application, version_int=version, **kwargs)

具体来说,您允许None的默认值为ApplicationVersion.application。我不确定这是为您提供什么值,因为默认的构造函数不需要您为模型的任何字段传递显式值,因此,如果未提供该值,则无论如何访问它都将是None

然后在测试的这一行:

python.versions.append(ApplicationVersion(version=27))

...由于构造函数的原因,您使用application=None显式创建了一个ApplicationVersion对象,但同时将其附加到python.versions集合中。这些关系解析外键值的方式似乎不一致,因此有时它尝试使用application_id=1刷新,它是新Application对象的pk,而其他时候则试图使用{ {1}}的构造方法。但是application_id=None不能为空:

application_id

...也就是您获得application_id = Column(Integer, ForeignKey('Application.row_id'), nullable=False) 的时候。

SQLAlchemy必须在将关系属性显式设置为IntegrityError与根本没有设置之间有所区别,因为如果您在构造函数中停止将None设置为application,则问题停止:

None

我能够将您的示例简化为这个通用示例(很抱歉,Python 3是这样,您需要调整def __init__(self, version, **kwargs): super(ApplicationVersion, self).__init__(version_int=version, **kwargs) 调用):

print

运行该命令时,应该获得成功和错误的随机计数。然后删除from sqlalchemy import create_engine, Column, Integer from sqlalchemy import ForeignKey from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship, sessionmaker engine = create_engine('sqlite://') Base = declarative_base() Session = sessionmaker(bind=engine) class Parent(Base): __tablename__ = 'parent' id = Column(Integer, primary_key=True) children = relationship('Child') class Child(Base): __tablename__ = 'child' id = Column(Integer, primary_key=True) num = Column(Integer) parent_id = Column(Integer, ForeignKey('parent.id'), nullable=False) parent = relationship('Parent') def __init__(self, parent=None, **kwargs): super(Child, self).__init__(parent=parent, **kwargs) if __name__ == '__main__': Base.metadata.create_all(engine) error_cnt = 0 success_cnt = 0 for _ in range(20): s = Session() try: parent = Parent() parent.children.append(Child()) s.add(parent) s.commit() except IntegrityError: error_cnt += 1 else: success_cnt += 1 finally: s.close() print('errors', error_cnt) print('successes', success_cnt) 方法,该方法将一直有效。