Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Django ORM have an equivalent to SQLAlchemy's Hybrid Attribute?

In SQLAlchemy, a hybrid attribute is either a property or method applied to an ORM-mapped class,

class Interval(Base):
    __tablename__ = 'interval'

    id = Column(Integer, primary_key=True)
    start = Column(Integer, nullable=False)
    end = Column(Integer, nullable=False)

    def __init__(self, start, end):
        self.start = start
        self.end = end

    @hybrid_property
    def length(self):
        return self.end - self.start

    @hybrid_method
    def contains(self,point):
        return (self.start <= point) & (point < self.end)

    @hybrid_method
    def intersects(self, other):
        return self.contains(other.start) | self.contains(other.end)

This allows for distinct behaviors at the class and instance levels, thus making it simpler to evaluate SQL statements using the same code,

>>> i1 = Interval(5, 10)
>>> i1.length
5

>>> print Session().query(Interval).filter(Interval.length > 10)
SELECT interval.id AS interval_id, interval.start AS interval_start,
interval."end" AS interval_end
FROM interval
WHERE interval."end" - interval.start > :param_1

Now in Django, if I have a property on a model,

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    def _get_full_name(self):
        "Returns the person's full name."
        return '%s %s' % (self.first_name, self.last_name)
    full_name = property(_get_full_name)

It is my understanding that I can not do the following,

Person.objects.filter(full_name="John Cadengo")

Is there an equivalent of SQLAlchemy's hybrid attribute in Django? If not, is there perhaps a common workaround?

like image 553
john Avatar asked Aug 31 '12 14:08

john


2 Answers

You're right that you cannot apply django queryset filter based on python properties, because filter operates on a database level. It seems that there's no equivalent of SQLAlchemy's hybrid attributes in Django.

Please, take a look here and here, may be it'll help you to find a workaround. But, I think there is no generic solution.

like image 88
alecxe Avatar answered Sep 21 '22 08:09

alecxe


One of the quick possible workarounds is to implement some descriptor, that would apply expressions via the annotation. Something like this:

from django.db import models
from django.db.models import functions


class hybrid_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__
        self.exp = None

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.func(instance)

    def __set__(self, instance, value):
        pass

    def expression(self, exp):
        self.exp = exp
        return self


class HybridManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        for name, value in vars(qs.model).items():
            if isinstance(value, hybrid_property) and value.exp is not None:
                qs = qs.annotate(**{name: value.exp(qs.model)})
        return qs


class TestAttribute(models.Model):
    val1 = models.CharField(max_length=256)
    val2 = models.CharField(max_length=256)

    objects = HybridManager()

    @hybrid_property
    def vals(self):
        return f"{self.val1} {self.val2}"

    @vals.expression
    def vals(cls):
        return functions.Concat(models.F("val1"), models.Value(" "), models.F("val2"))


class HybridTests(TestCase):
    def setUp(self) -> None:
        self.test_attr = TestAttribute.objects.create(val1="val1", val2="val2")

    def test_access(self):
        self.assertTrue(TestAttribute.objects.exists())
        self.assertEqual(self.test_attr.vals, f"{self.test_attr.val1} {self.test_attr.val2}")
        self.assertTrue(TestAttribute.objects.filter(vals=f"{self.test_attr.val1} {self.test_attr.val2}").exists())
        self.assertTrue(TestAttribute.objects.filter(vals__iexact=f"{self.test_attr.val1} {self.test_attr.val2}").exists())


like image 44
Artem Merkulov Avatar answered Sep 19 '22 08:09

Artem Merkulov