SQL Alchemy中的NULL安全不等式比较?

nco*_*lan 8 python sqlalchemy

目前,我知道如何!=在SQL Alchemy中表达NULL安全比较的唯一方法(其中与NULL条目的比较评估为True,而不是NULL)是:

or_(field == None, field != value)
Run Code Online (Sandbox Code Playgroud)

有没有办法在SQL Alchemy中表达这一点,它会发出一个MySQL <=>操作或PostgreSQL IS DISTINCT FROM操作,将NULL视为另一个可能的值?

附加背景

这是一个实用程序中的错误,它从基于XML的查询语言转换为SQL Alchemy过滤器表达式:http://gerrit.beaker-project.org/#/c/2776/

具体的bug出现在一段代码中,如下所示:

query = getattr(field, op)(value)
Run Code Online (Sandbox Code Playgroud)

"field"是相关的SQL Alchemy模型列,"op"是比较操作的相关Python属性名称,"value"是要检查的值.例如,在对具有特定名称的虚拟机管理程序上运行的虚拟机进行过滤的特定情况下,它将等同于:

query = Hypervisor.hypervisor.__eq__("KVM")
Run Code Online (Sandbox Code Playgroud)

该错误不是由于可能存在的NULL(不会发生 - 值始终为字符串),而是在与包含NULL值且比较运算符为的行进行比较时__ne__.

对于除了之外的每个运算符__ne__,标准SQL NULL处理工作正常(比较返回NULL,这被解释为与过滤器不匹配的行,这是我们想要的).但是,对于__ne__我们确实想要返回该列中包含NULL值的行的情况- 我们只想排除设置值的那些行,并且它与我们要比较的值不匹配.

所以最初的代码如下:

query = getattr(field, op)(value)
Run Code Online (Sandbox Code Playgroud)

现在看起来更像:

if op == "__ne__":
    query = or_(field == None, field != value)
else:
    query = getattr(field, op)(value)
Run Code Online (Sandbox Code Playgroud)

这对我来说似乎很笨拙,所以我问是否有一种方法我们可以将"!="映射到其他__ne__方法NULL,而不是在数据库层给我们替代处理,而不是用SQL Alchemy等效的方式来模拟它field IS NOT NULL OR field != value(因为我们已经开始做了).

zzz*_*eek 7

有几种方法可以在其中插入备用运算符,以及创建自定义运算符,但是获取__ne__()调用时所发生的事情的最公开/主流方式是在类型级别:

from sqlalchemy import TypeDecorator, type_coerce, String, or_

class NullComparisons(TypeDecorator):
    impl = String

    class comparator_factory(TypeDecorator.Comparator):
        def __ne__(self, other):
            expr = type_coerce(self.expr, String)
            return or_(expr == None, expr != other)
Run Code Online (Sandbox Code Playgroud)

所以这将做OR事情:

from sqlalchemy import Column, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
class Thing(Base):
    __tablename__ = 'thing'

    id = Column(Integer, primary_key=True)
    data = Column(NullComparisons(50))
print(Thing.data != 'hi')
Run Code Online (Sandbox Code Playgroud)

给我们:

thing.data IS NULL OR thing.data != :param_1
Run Code Online (Sandbox Code Playgroud)

那么对于PG/MySQL运算符,我们真正需要的是能够直接将@compiles链接到运算符.但是这个钩子现在还没有出现,所以比理想情况下需要更多努力,我们可以创建一个自定义列元素来处理它:

from sqlalchemy import TypeDecorator, type_coerce, String
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import BinaryExpression
import operator
class IsDistinctFrom(BinaryExpression):
    pass

@compiles(IsDistinctFrom, "postgresql")
def pg_is_distinct_from(element, compiler, **kw):
    return "%s IS DISTINCT FROM %s" % (
                    compiler.process(element.left, **kw),
                    compiler.process(element.right, **kw),
                )

@compiles(IsDistinctFrom, "mysql")
def mysql_is_distinct_from(element, compiler, **kw):
    return "%s <=> %s" % (
                    compiler.process(element.left, **kw),
                    compiler.process(element.right, **kw),
                )

class AdvancedNullComparisons(TypeDecorator):
    impl = String

    class comparator_factory(TypeDecorator.Comparator):
        def __ne__(self, other):
            expr = type_coerce(self.expr, String)
            # this step coerces a literal into a SQL expression,
            # this can be done without the private API here but the private
            # function does the most thorough job, this could also be made
            # public
            other = self._check_literal(expr, operator.ne, other)
            return IsDistinctFrom(self.expr, other, operator.ne)
Run Code Online (Sandbox Code Playgroud)

那我们可以尝试一下:

from sqlalchemy import Column, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Thing(Base):
    __tablename__ = 'thing'

    id = Column(Integer, primary_key=True)
    data = Column(AdvancedNullComparisons(50))

from sqlalchemy.dialects import postgresql, mysql
print(Thing.data != 'hi').compile(dialect=postgresql.dialect())
print(Thing.data != 'hi').compile(dialect=mysql.dialect())
Run Code Online (Sandbox Code Playgroud)

给我们:

thing.data IS DISTINCT FROM %(param_1)s
thing.data <=> %s
Run Code Online (Sandbox Code Playgroud)

  • 看起来 SQLAlchemy 1.1 添加了 [.isnot_distinct_from()](http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.ColumnElement.isnot_distinct_from) 和 [.is_distinct_from() ](http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.operators.ColumnOperators.is_distinct_from) 对于这些,这很方便。 (3认同)