SQLAlchemy 似乎只会抛出一般性的
IntegrityError
。当然,确切的查询和错误消息包含在异常中,这足以供人调试程序。然而,在为异常编写错误处理代码时,据我所知,似乎没有一个好的方法来检查哪个表上的哪个约束导致了错误。另外,异常是由 session.commit()
行引发的,而不是实际负责产生错误的行,因此我也无法使用多个 try/ except 块来区分。
有没有一种方法,除了尝试以编程方式解析错误消息和/或查询之外,我可以将重复的主键错误与外键错误或失败的 CHECK 约束等区分开来?或者甚至只是一种判断哪个表的哪一列违反了数据完整性的方法?或者只是一种在导致错误的行上立即引发异常而不是等待事务提交的方法?
IntegrityError
实例具有orig
和statement
属性,可以检查它们以分别获取错误消息和失败的SQL语句。
鉴于此模型:
class Foo(Base):
__tablename__ = 'foo20201209'
id = sa.Column(sa.Integer, primary_key=True)
bar = sa.Column(sa.String(2), unique=True)
baz = sa.Column(sa.Integer, sa.CheckConstraint('baz >= 0'), default=0)
此代码:
conn_strings = ['postgresql+psycopg2:///test',
'mysql+mysqlconnector:///test',
'sqlite://']
for cs in conn_strings:
engine = sa.create_engine(cs)
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
session = orm.Session(bind=engine)
for kwds in [{'bar': 'a'}, {'bar': 'a'}, {'bar': 'b', 'baz': -11}]:
session.add(Foo(**kwds))
try:
session.commit()
except sa.exc.IntegrityError as ex:
print(ex.orig)
print(ex.statement)
print()
session.rollback()
session.close()
engine.dispose()
将产生以下输出:
duplicate key value violates unique constraint "foo20201209_bar_key"
DETAIL: Key (bar)=(a) already exists.
INSERT INTO foo20201209 (bar, baz) VALUES (%(bar)s, %(baz)s) RETURNING foo20201209.id
new row for relation "foo20201209" violates check constraint "foo20201209_baz_check"
DETAIL: Failing row contains (3, b, -11).
INSERT INTO foo20201209 (bar, baz) VALUES (%(bar)s, %(baz)s) RETURNING foo20201209.id
1062 (23000): Duplicate entry 'a' for key 'bar'
INSERT INTO foo20201209 (bar, baz) VALUES (%(bar)s, %(baz)s)
4025 (23000): CONSTRAINT `foo20201209.baz` failed for `test`.`foo20201209`
INSERT INTO foo20201209 (bar, baz) VALUES (%(bar)s, %(baz)s)
UNIQUE constraint failed: foo20201209.bar
INSERT INTO foo20201209 (bar, baz) VALUES (?, ?)
CHECK constraint failed: foo20201209
INSERT INTO foo20201209 (bar, baz) VALUES (?, ?)
正如我原来的答案下面的DMJ评论,不同的数据库引擎在发生完整性错误时会发出不同的错误消息,并且SQLAlchemy不会尝试以一致的方式呈现这些消息,因此对问题的简短回答是:如果不解析错误消息,就无法从异常中获取有用的信息。
也就是说,如果约束以一致的方式命名,并且这些名称出现在错误消息中,则解析错误消息可能不会那么困难。这里 SQLAlchemy 提供了一些帮助:我们可以创建一个 Metadata 对象,并使用定义的约束的命名约定(docs)。有了这个约定,我们创建的任何约束都将遵循它,我们可以匹配错误消息中的约束名称,并使用匹配来查找元数据中的表和约束对象。
以下是如何使用约定解析错误消息的示例。我没有涵盖所有可能的约束类型,也没有处理 Sqlite 的唯一键违规消息,该消息省略了约束名称。这些留给读者作为练习;-)
import re
import sqlalchemy as sa
convention = {
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
}
metadata = sa.MetaData(naming_convention=convention)
tbl = sa.Table(
't65189213',
metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('foo', sa.String(2), unique=True),
sa.Column('bar', sa.Integer, default=0),
sa.CheckConstraint('bar >= 0', name='positive_bar'),
)
# Basic pattern to match constraint names (not exhaustive).
pattern = r'(?P<type_key>uq|ck)_(?P<table>[a-z][a-z0-9]+)_[a-z_]+'
regex = re.compile(pattern)
def parse_exception(ex):
# Returns an informative message, or the original error message.
# We could return the table, constraint object etc. instead.
types = {'ck': 'Check constraint', 'uq': 'Unique key'}
m = regex.search(str(ex.orig))
if m:
type_ = types[m.groupdict()['type_key']]
table_name = m.groupdict()['table']
constraint_name = m.group(0)
table = metadata.tables[table_name]
constraint = next(c for c in table.constraints if c.name == constraint_name)
columns = ','.join(constraint.columns.keys())
return f'{type_} {constraint_name} has been violated on table {table_name!r} columns: {columns}'
return f'{ex.orig}'
conn_strings = ['postgresql+psycopg2:///test',
'mysql+mysqlconnector://root:root@localhost/test',
'sqlite://']
for cs in conn_strings:
engine = sa.create_engine(cs, future=True)
tbl.drop(engine, checkfirst=True)
tbl.create(engine)
with engine.connect() as conn:
print(engine.dialect.name)
for kwds in [{'foo': 'a'}, {'foo': 'a'}, {'foo': 'b', 'bar': -11}]:
try:
conn.execute(tbl.insert(), kwds)
conn.commit()
except sa.exc.IntegrityError as ex:
print(parse_exception(ex))
conn.rollback()
print('-'*10)
engine.dispose()
输出:
postgresql
Unique key uq_t65189213_foo has been violated on table 't65189213' columns: foo
Check constraint ck_t65189213_positive_bar has been violated on table 't65189213' columns:
----------
mysql
Unique key uq_t65189213_foo has been violated on table 't65189213' columns: foo
Check constraint ck_t65189213_positive_bar has been violated on table 't65189213' columns:
----------
sqlite
UNIQUE constraint failed: t65189213.foo
Check constraint ck_t65189213_positive_bar has been violated on table 't65189213' columns:
----------
我最终使用
session.flush()
来提前触发异常。我在有问题的行之前调用它一次(所以我100%确定该异常不是由前面的行触发的),然后在 try/catch 块中再次调用它以查看该行是否在问题导致错误。
我管理我对这个解决方案并不完全满意,但我还没有找到其他任何东西。我仍然很想听听是否有更好的解决方案,最好能够准确地告诉我哪个表的哪个约束导致了错误。但是,这是一种可能对某人有所帮助的解决方法。
SQLAlchemy
==1.4.28,
UNIQUE
约束名称仍然没有出现在抛出的异常中。考虑下一个例子:
class M(Model):
a = Column(String)
i = Column(Integer)
UniqueConstraint(M.i, name="my unique")
CheckConstraint(0 <= M.i, name="my check")
def test_test():
db = create_test_database(Model)
try:
with db.session() as s:
# Here we break our UNIQUE constraint
try:
with s.begin_nested():
s.add_all(
[
M(a="Alice", i=1),
M(a="Bob", i=1),
]
)
except IntegrityError as err:
rich.inspect(err)
# Here we break our CHECK constraint
s.add(M(a="Alice", i=-1))
except IntegrityError as err:
rich.inspect(err)
结果如下:
┌────────────────────── <class 'sqlalchemy.exc.IntegrityError'> ───────────────────────┐
│ Wraps a DB-API IntegrityError. │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
│ │ IntegrityError('(sqlite3.IntegrityError) UNIQUE constraint failed: M.i') │ │
│ └──────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ args = ('(sqlite3.IntegrityError) UNIQUE constraint failed: M.i',) │
│ code = 'gkpj' │
│ connection_invalidated = False │
│ detail = [] │
│ hide_parameters = False │
│ ismulti = False │
│ orig = IntegrityError('UNIQUE constraint failed: M.i') │
│ params = ('Bob', 1) │
│ statement = 'INSERT INTO "M" (a, i) VALUES (?, ?)' │
└──────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────── <class 'sqlalchemy.exc.IntegrityError'> ─────────────────────────┐
│ Wraps a DB-API IntegrityError. │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ IntegrityError('(sqlite3.IntegrityError) CHECK constraint failed: my check') │ │
│ └──────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ args = ('(sqlite3.IntegrityError) CHECK constraint failed: my check',) │
│ code = 'gkpj' │
│ connection_invalidated = False │
│ detail = [] │
│ hide_parameters = False │
│ ismulti = False │
│ orig = IntegrityError('CHECK constraint failed: my check') │
│ params = ('Alice', -1) │
│ statement = 'INSERT INTO "M" (a, i) VALUES (?, ?)' │
└──────────────────────────────────────────────────────────────────────────────────────────┘
因此,看起来这个 CHECK
约束名称将始终位于异常字符串中的某个位置,您可以围绕它编写代码。很明显,为什么这是
UNIQUE
约束的额外信息,因为您已经有了单词
UNIQUE
和字段名称(在我的示例中为
A.i
)。我认为他们永远不会改变这些字符串格式,但询问他们会很有趣。
警告:
问题是我的代码与 SQLite
相关,而您的代码可能与另一个数据库相关,并且这些消息会有所不同,因为它们最初来自底层数据库引擎,而不是来自
SQLAlchemy
本身。因此,您必须注意抽象这些字符串的代码。
e
:使用内部结构可能不是一个好主意,因为它们可能会发生变化,但我们可以以安全的方式编写代码来检查它们(例如 getattr 并在出现问题时提供带有日志记录的默认情况)。
本质上,这就是我想出的。实际上,我的代码中有更多检查以使其达到生产级。
注意:这是 Postgres 特有的!
from sqlalchemy.exc import IntegrityError, MultipleResultsFound, NoResultFound
from asyncpg import NotNullViolationError, UniqueViolationError
try:
...
except IntegrityError as e:
cause = e.orig.__getattribute__("__cause__")
# If cause is None, just default to constraint violation error
if cause is None:
raise ConstraintViolationError(e)
if isinstance(cause, NotNullViolationError):
# Can't type this well as asyncpg error attributes are set dynamically by a metaclass
raise ConstraintViolationError(cause.__getattribute__("message"))
elif isinstance(cause, UniqueViolationError):
column = cause.__getattribute__("column_name")
raise NotUniqueError(
message=cause.__getattribute__("message"), model_name=self.model.__name__, column_name=column
)
else:
column = cause.__getattribute__("column_name")
raise ConstraintViolationError(cause.__getattribute__("message"))