Python 使SQLAlchemy错误更加用户友好和详细
我有一个这样的模型:Python 使SQLAlchemy错误更加用户友好和详细,python,flask,sqlalchemy,flask-sqlalchemy,Python,Flask,Sqlalchemy,Flask Sqlalchemy,我有一个这样的模型: class Company(db.Model): __tablename__ = "my_table" id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(128), unique=True, nullable=False) slug = db.Column(db.String(128), unique=True, nullable=False)
class Company(db.Model):
__tablename__ = "my_table"
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(128), unique=True, nullable=False)
slug = db.Column(db.String(128), unique=True, nullable=False)
如您所见,我使用的是SQLAlchemy
的类和方法,但这不是重点,
假设我有一个执行以下行的视图:
c = Company("Test", "test")
try:
db.session.add(c)
db.session.commit()
return "Added!"
except Exception as e:
db.session.rollback()
return f"{e}"
上面的代码创建了一个公司
的对象,尝试将其添加到数据库,在异常时回滚事务
问题就在这里,因为数据是硬编码的,所以它应该总是返回异常,SQLAlchemy会引发异常
IntegrityError
对用户来说是如此丑陋和无用,例如:
(sqlite3.IntegrityError) UNIQUE constraint failed: my_table.name [SQL: 'INSERT INTO my_table (name, slug) VALUES (?, ?)'] [parameters: ('Test', 'tests')] (Background on this error at: http://sqlalche.me/e/gkpj)
我正在寻找一种方法,使其美观且用户友好,在此之前我使用的是db.validates
decorator,并在验证时检查重复数据,但我觉得这不对
我需要做的最简单的事情是在没有硬编码的情况下找出导致问题的字段。一般来说,您可以
尝试/except
错误以“捕获”它,记录错误,然后向用户返回自定义错误。像这样:
c=公司(“测试”、“测试”)
尝试:
db.session.add(c)
db.session.commit()
return“已添加!”
例外情况除外,如e:
db.session.rollback()
返回f“未能插入公司:{e.\\\\\\\\\\\\\\\\\\\\\\\\\\}”
这似乎是一个快速的答案,但不是正确的答案。相反,我会在尝试插入之前添加一些验证:
c=公司(“测试”、“测试”)
#注意,这是伪代码
如果公司。查找。获取(“测试”):
尝试:
db.session.add(c)
db.session.commit()
return“已添加!”
例外情况除外,如e:
db.session.rollback()
返回f“未能插入公司:{e.\\\\\\\\\\\\\\\\\\\\\\\\\\}”
其他:
返回f“公司{c.id}已存在”
这样,您就不会引发错误,相反,您的应用程序正在处理其数据,而不是插入失败。您可以导入
异常,然后自己处理它:
from sqlite3.__init__ import IntegrityError
这将为您提供异常名称,然后您可以执行以下操作:
except IntegrityError :
db.session.rollback()
return f"duplicate data has been used!"
或者,您需要处理此异常
但是请记住,如果您使用的是sqlite
包,而不是sqlalchemy
,那么这只会捕获错误。因此,如果您在某个时候更改了db引擎
,您可能无法处理此异常
from sqlalchemy.exc import IntegrityError
是您需要为sqlalchemy
引发的异常而引发的异常class
。sqlalchemy包含一种允许使用事件挂钩自定义DBAPI错误的机制。我在Openstack中使用了这个API,可以在以下文件中看到:
由于stackoverflow讨厌代码链接,下面是基于上述链接方法的POC:
import collections
from sqlalchemy import event
from sqlalchemy import exc as sqla_exc
import re
class DuplicateKeyError(Exception):
"""Duplicate entry at unique column error."""
def __init__(self, columns=None, inner_exception=None, value=None):
self.columns = columns or []
self.value = value
self.inner_exception = inner_exception
def __str__(self):
return "Duplicate key for columns %s" % (
self.columns,
)
_registry = collections.defaultdict(lambda: collections.defaultdict(list))
def filters(ame, exception_type, regex):
"""Mark a function as receiving a filtered exception."""
def _receive(fn):
_registry[ame][exception_type].extend(
(fn, re.compile(reg))
for reg in ((regex,) if not isinstance(regex, tuple) else regex)
)
return fn
return _receive
# each @filters() lists a database name, a SQLAlchemy exception to catch,
# and a list of regular expressions that will be matched. If all the
# conditions match, the handler is called which then raises a nicer
# error message.
@filters(
"sqlite",
sqla_exc.IntegrityError,
(
r"^.*columns?(?P<columns>[^)]+)(is|are)\s+not\s+unique$",
r"^.*UNIQUE\s+constraint\s+failed:\s+(?P<columns>.+)$",
r"^.*PRIMARY\s+KEY\s+must\s+be\s+unique.*$",
),
)
def _sqlite_dupe_key_error(integrity_error, match, engine_name, is_disconnect):
columns = []
try:
columns = match.group("columns")
columns = [c.split(".")[-1] for c in columns.strip().split(", ")]
except IndexError:
pass
raise DuplicateKeyError(columns, integrity_error)
@filters(
"mysql",
sqla_exc.IntegrityError,
r"^.*\b1062\b.*Duplicate entry '(?P<value>.*)'"
r" for key '(?P<columns>[^']+)'.*$",
)
@filters(
"postgresql",
sqla_exc.IntegrityError,
(
r'^.*duplicate\s+key.*"(?P<columns>[^"]+)"\s*\n.*'
r"Key\s+\((?P<key>.*)\)=\((?P<value>.*)\)\s+already\s+exists.*$",
r"^.*duplicate\s+key.*\"(?P<columns>[^\"]+)\"\s*\n.*$",
),
)
def _default_dupe_key_error(
integrity_error, match, engine_name, is_disconnect
):
columns = match.group("columns")
uniqbase = "uniq_"
if not columns.startswith(uniqbase):
if engine_name == "postgresql":
columns = [columns[columns.index("_") + 1 : columns.rindex("_")]]
else:
columns = [columns]
else:
columns = columns[len(uniqbase) :].split("0")[1:]
value = match.groupdict().get("value")
raise DuplicateKeyError(columns, integrity_error, value)
def handler(context):
"""Iterate through available filters and invoke those which match.
The first one which raises wins.
"""
def _dialect_registries(engine):
if engine.dialect.name in _registry:
yield _registry[engine.dialect.name]
if "*" in _registry:
yield _registry["*"]
for per_dialect in _dialect_registries(context.engine):
for exc in (context.sqlalchemy_exception, context.original_exception):
for super_ in exc.__class__.__mro__:
if super_ in per_dialect:
regexp_reg = per_dialect[super_]
for fn, regexp in regexp_reg:
match = regexp.match(exc.args[0])
if match:
fn(
exc,
match,
context.engine.dialect.name,
context.is_disconnect,
)
if __name__ == '__main__':
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Company(Base):
__tablename__ = "my_table"
id = Column(Integer(), primary_key=True)
name = Column(String(128), unique=True, nullable=False)
slug = Column(String(128), unique=True, nullable=False)
def __init__(self, name, slug):
self.name = name
self.slug = slug
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
event.listen(e, "handle_error", handler)
s = Session(e)
s.add(Company("Test", "test"))
s.commit()
s.add(Company("Test", "test"))
s.commit()
感谢您的快速回答,第二种方法与我在验证方法中的检查类似。您的第二种方法可能仍然会引发错误,因为在检查和尝试插入之间可能会提交另一个请求,因此最好只处理约束冲突。@IljaEverilä目前我使用的是db.validates
decorator,如果发现另一行具有相同的数据,我会提出一个ValueError
并在视图中处理它,方法是将对象初始化放在try/except
块中,但我正在寻找一种更好的方法。这似乎是我需要的答案,我会尽快尝试
2019-03-13 09:44:51,701 INFO sqlalchemy.engine.base.Engine INSERT INTO my_table (name, slug) VALUES (?, ?)
2019-03-13 09:44:51,701 INFO sqlalchemy.engine.base.Engine ('Test', 'test')
2019-03-13 09:44:53,387 INFO sqlalchemy.engine.base.Engine ROLLBACK
Traceback (most recent call last):
# ...
sqlite3.IntegrityError: UNIQUE constraint failed: my_table.slug
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
# ...
__main__.DuplicateKeyError: Duplicate key for columns ['slug']