简体   繁体   中英

How to select different type of columns dynamically to use in where clause?

I have a following table in sqlalchemy:

class FieldType(enum.Enum):
   INT_FIELD = 0
   FLOAT_FIELD = 1
   STRING_FIELD = 2

class EAVTable(Base):
     __tablename__ = 'EAVTable'

     field_name = Column(Stirng, primary_key=True)
     field_type = Column(Enum(FieldType))
     int_field = Column(Integer)
     float_field = Column(Float)
     string_field = Column(String)

This is to model the EAV model which fits my business purpose.

Now to use it easily in the code I have the following hybrid_property .

@hybrid_propderty
def value(self):
    if self.field_type == FieldType.INT_FIELD:
         return self.int_field
    ...

@value.setter
def value(self, value):
    if type(value) == int:
        self.field_type = FieldType.INT_FIELD
        self.int_field = value
    ...

This works fine when I try to get and set the fields in Python code. But I still have a problem:

session.query(EAVTable).filter(EAVTable.value == 123) 

This does not work out of the box but I had an idea of using hybrid.expression where we use a case statement:

@value.expression
def value(cls):
    return case(
        [
            (cls.field_type == FieldType.INT_FIELD, cls.int_field),
            (cls.field_type == FieldType.FLOAT_FIELD, cls.float_field),
            ...
        ]
    )

This in theory works, for example, the SQL generated for query session.query(EAVTable.value = 123 looks like:

select * from where case 
    when field_type = INT_FIELD then int_field
    when field_type = FLOAT_FIELD then float_field
    when field_type = STRING_FIELD then string_field 
    end = 123;

Which semantically looks like what I like, but later I find that the case expression requires all the cases have the same type, or they are cast into the same type.

I understand this is a requirement from the SQL language and has nothing to do with sqlachemy, but for more seasoned sqlalchemy user, is there any easy way to do what I want to achieve? Is there a way to walk around this constraint?

You could move the comparison inside the CASE expression using a custom comparator :

from sqlalchemy.ext.hybrid import Comparator

class PolymorphicComparator(Comparator):
    def __init__(self, cls):
        self.cls = cls

    def __clause_element__(self):
        # Since SQL doesn't allow polymorphism here, don't bother trying.
        raise NotImplementedError(
            f"{type(self).__name__} cannot be used as a clause")

    def operate(self, op, other):
        cls = self.cls
        return case(
            [
                (cls.field_type == field_type, op(field, other))
                for field_type, field in [
                    (FieldType.INT_FIELD, cls.int_field),
                    (FieldType.FLOAT_FIELD, cls.float_field),
                    (FieldType.STRING_FIELD, cls.string_field),
                ]
            ],
            else_=False
        )

class EAVTable(Base):
     ...

     # This replaces @value.expression
     @value.comparator
     def value(cls):
         return PolymorphicComparator(cls)

This way the common type is just boolean.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM