【问题标题】:Make SQLAlchemy Errors more user friendly and detailed使 SQLAlchemy 错误更加用户友好和详细
【发布时间】:2019-03-13 02:19:38
【问题描述】:

我有一个这样的模型:

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)

如您所见,我正在使用Flask-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}"

上面的代码,创建了一个Company的对象,尝试将它添加到数据库中,在异常时回滚事务,

问题就在这里,因为数据是硬编码的,它应该总是返回异常,SQLAlchemy 会引发IntegrityError

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 装饰器并检查验证时的重复数据,但这对我来说感觉不对 p>

我最不需要的就是找出导致问题的字段而无需硬编码

【问题讨论】:

    标签: python flask sqlalchemy flask-sqlalchemy


    【解决方案1】:

    SQLAlchemy 包含一种机制,允许使用 handle_error 事件挂钩自定义 DBAPI 错误。我在 Openstack oslo.db 中使用了这个 API,可以在这个文件中看到:https://github.com/openstack/oslo.db/blob/master/oslo_db/sqlalchemy/exc_filters.py

    由于 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()
    

    运行它,我们看到:

    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']
    

    【讨论】:

    • 这似乎是我需要的答案,我会尽快尝试一下
    【解决方案2】:

    一般来说,您可以try/except 错误来“捕捉”它,记录错误,然后将自定义错误返回给用户。像这样:

    c = Company("Test", "test")
    try:
        db.session.add(c)
        db.session.commit()
        return "Added!"
    except Exception as e:
        db.session.rollback()
        return f"failed to insert company: {e.__class__.__name__}"
    

    这似乎是一个快速的答案,而不是正确的答案。相反,我会在尝试插入之前添加一些验证:

    c = Company("Test", "test")
    # note this is pseudo code
    if Company.find.get("Test"):
        try:
            db.session.add(c)
            db.session.commit()
            return "Added!"
        except Exception as e:
            db.session.rollback()
            return f"failed to insert company: {e.__class__.__name__}"
    else:
    return f"company {c.id} already exists"
    

    这样您就不会引发错误,而是您的应用程序正在处理其数据而不是插入失败。

    【讨论】:

    • 感谢您的快速回答,第二种方式与我在验证方法中的检查方式相同
    • 您的第二种方式可能仍然会引发错误,因为在检查和尝试插入之间可能会提交另一个请求,因此最好只处理约束违规。
    • @IljaEverilä 目前我正在使用db.validates 装饰器,如果找到具有相同数据的另一行,我会提出一个ValueError 并通过将对象初始化放入try/except 在视图中处理它阻止,但我正在寻找更好的方法
    【解决方案3】:

    您可以导入exception,然后自己处理:

    from sqlite3.__init__ import IntegrityError
    

    这将为您提供异常名称,然后您可以执行以下操作:

    except IntegrityError :
        db.session.rollback()
        return f"duplicate data has been used!"
    

    否则您需要处理此异常。

    请记住,这只会在您使用 sqlite 包而不是 sqlalchemy 时捕获错误,因此如果您在途中更改了 db engine,您可能无法处理此 @ 987654327@.

    from sqlalchemy.exc import IntegrityError
    

    是您需要为 sqlalchemy 引发的异常引发的异常 class

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-18
      • 1970-01-01
      • 1970-01-01
      • 2011-02-16
      • 2014-06-03
      • 2019-10-12
      • 2012-04-06
      • 1970-01-01
      相关资源
      最近更新 更多