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
这些可以阻止错误的动作如下:
BaseTable
或Session
删除文档字符串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如此表现。
答案 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)
方法,该方法将一直有效。