SQLAlchemy 多对多 LEFT OUTER JOIN 与排序的实体列表

SQLAlchemy many-to-many LEFT OUTER JOIN with sorted list of entities

我有两个表:居民和过敏原以及两者之间的多对多关系。在 PostgreSQL 上使用 SQLAlchemy,我想获取按字母顺序排列的过敏原列表排序的居民列表:

 resident_id | resident_name |      allergs       
-------------+---------------+--------------------
           1 | John          | {milk,pollen,soy}
           3 | Hopkins       | {pollen,stupidity}
           2 | Mary          | {stupidity}
           4 | Lee           | {NULL}

我知道这可以在 PostgreSQL 中使用 array_agg:

SELECT
  resident.id AS resident_id,
  resident.name AS resident_name,
  array_agg(allergen.NAME ORDER BY allergen.NAME) AS allergs
FROM resident
LEFT OUTER JOIN (resident_allergens AS resident_allergens_1
JOIN allergen
  ON allergen.id = resident_allergens_1.allergen_id)
  ON resident.id = resident_allergens_1.resident_id
GROUP BY resident.id
ORDER BY allergs

但据我所知,SQLAlchemy 不支持 array_agg 函数中的 ORDER BY 子句。

到目前为止,我已经尝试过:

Table 定义如下:

resident_allergens = db.Table(
    'resident_allergens',
    db.Column('resident_id', db.Integer, db.ForeignKey('resident.id'), nullable=False, index=True),
    db.Column('allergen_id', db.Integer, db.ForeignKey('allergen.id'), nullable=False, index=True),
    UniqueConstraint('resident_id', 'allergen_id'))


class Allergen(db.Model):
    id = Column(db.Integer, primary_key=True)
    name = Column(db.String)

    def __init__(self, name):
        self.name = name


class Resident(db.Model):
    id = Column(db.Integer, primary_key=True)
    name = Column(db.String)

    allergies = db.relationship('Allergen',
                                collection_class=set,
                                secondary=resident_allergens,
                                backref=db.backref('residents', lazy='lazy'))

    def __init__(self, name):
        self.name = name

如果相关,我使用 SQLAlchemy 0.8.5 而不是 PostgreSQL 9.3

使用 SQLAlchemy 的 compilation extension,我能够添加我自己的 array_agg 版本,支持 ORDER BY:

from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import ColumnClause, _literal_as_binds


class array_agg(ColumnClause):
    """Custom version of PostgreSQL's array_agg with support for ORDER BY.

    Usage: ... .order_by(array_agg(Allergen.name, order_by=Allergen.name))
    """

    def __init__(self, expr, order_by=None):
        self.expr = _literal_as_binds(expr)
        self.order_by = _literal_as_binds(order_by)

    @property
    def _from_objects(self):
        return self.expr._from_objects


@compiles(array_agg)
def compile_array_agg(element, compiler, **kwargs):
    head = 'array_agg(%s' % (
        compiler.process(element.expr),
    )
    if element.order_by is not None:
        tail = ' ORDER BY %s)' % compiler.process(element.order_by)
    else:
        tail = ')'
    return head + tail