У меня есть следующий самоссылочный (древовидный) узел, и я хочу фильтровать / сортировать по вычисленным свойствам uuid_path и name_path:

class Node (db.Model):
    id = db.Column (db.Integer, db.Sequence ('node_id_seq'), primary_key=True)

    ###########################################################################

    root_id = db.Column (db.Integer, db.ForeignKey (id, ondelete='CASCADE'),
        index=True)

    nodes = db.relationship ('Node',
        cascade='all, delete-orphan', lazy='dynamic',
        primaryjoin='Node.id==Node.root_id',
        backref=db.backref ('root', remote_side=id))

    ###########################################################################

    _uuid = db.Column (db.String (36), nullable=False, index=True, unique=True,
        name = 'uuid')
    _name = db.Column (db.Unicode (256), nullable=False, index=True,
        name = 'name')

    ###########################################################################

    @hybrid_property
    def uuid (self):
        return self._uuid

    @hybrid_property
    def name (self):
        return self._name
    @name.setter
    def name (self, value):
        self._name = value

    ###########################################################################

    def __init__ (self, name, root, mime=None, uuid=None):

        self.root = root
        self._uuid = uuid if uuid else str (uuid_random ())
        self._name = unicode (name) if name is not None else None

    def __repr__ (self):

        return u'<Node@%x: %s>' % (self.id if self.id else 0, self._name)

    ###########################################################################

    @hybrid_property
    def uuid_path (self):
        node, path = self, []
        while node:

            path.insert (0, node.uuid)
            node = node.root

        return os.path.sep.join (path)

    @hybrid_property
    def name_path (self):
        node, path = self, []
        while node:

            path.insert (0, node.name)
            node = node.root

        return os.path.sep.join (path)

    ###########################################################################

Если я получу Node экземпляр subnode и выполню, например, subnode.name_path тогда я получаю правильно, например root/subnode . Но если я пытаюсь использовать Node.name_path (для фильтрации / сортировки), SQLAlchemy жалуется:

Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with Node.root has an attribute 'name'.

Я уверен, что я должен представить что-то вроде:

class Node (db.Model):

    @hybrid_property
    def name_path (self):
        node, path = self, []
        while node:

            path.insert (0, node.name)
            node = node.root

        return os.path.sep.join (path)

    @name_path.expression
    def name_path (cls):
        ## Recursive SQL expression??

Но я изо всех сил пытаюсь получить правильное определение для @name_path.expression (или @uuid_path.expression); он должен каким-то образом инструктировать SQL доставлять путь от корневого узла до рассматриваемого узла.

Чего я не понимаю, так это того, почему это требуется, так как я сказал SQLAlchemy итеративно вычислять значения пути. Спасибо за вашу помощь.

2
hsk81 23 Янв 2013 в 23:03

2 ответа

Лучший ответ

После настройки PostgreSQL и SQLAlchemy, я думаю, у меня есть решение: (1) во-первых, я бы написал запрос как функцию в SQL, и (2) во-вторых, предоставил бы правильный клей SQLAlchemy:

Часть SQL использует WITH RECURSIVE CTE:

CREATE OR REPLACE FUNCTION name_path(node)
  RETURNS text AS
$BODY$

WITH RECURSIVE graph (id, root_id, id_path, name_path) AS (
    SELECT n.id, n.root_id, ARRAY[n.id], ARRAY[n.name]
    FROM node n
UNION
    SELECT n.id, n.root_id, id_path||ARRAY[n.id], name_path||ARRAY[n.name]
    FROM node n, graph g
    WHERE n.root_id = g.id)

SELECT array_to_string (g.name_path, '/','.*')
FROM graph g
WHERE (g.id_path[1] = $1.base_id OR g.root_id IS NULL)
AND (g.id = $1.id)

$BODY$
  LANGUAGE sql STABLE
  COST 100;
ALTER FUNCTION name_path(node)
  OWNER TO webed;

И сторона SQLAlchemy выглядит так:

class NamePathColumn (ColumnClause):
    pass

@compiles (NamePathColumn)
def compile_name_path_column (element, compiler, **kwargs):
    return 'node.name_path' ## something missing?

А также

class Node (db.Model):

    def get_path (self, field):

        @cache.version (key=[self.uuid, 'path', field])
        def cached_path (self, field):

            if self.root:
                return self.root.get_path (field) + [getattr (self, field)]
            else:
                return [getattr (self, field)]

        if field == 'uuid':
            return cached_path (self, field)
        else:
            return cached_path.uncached (self, field)

    @hybrid_property
    def name_path (self):
        return os.path.sep.join (self.get_path (field='name'))

    @name_path.expression
    def name_path (cls):
        return NamePathColumn (cls)

Я избегаю доступа к Node.name_path в БД, если я на стороне чистого Python, но, вероятно, было бы быстрее, если бы я это сделал. Единственное, в чем я до сих пор не уверен, так это в compile_name_path_column, я не рассматриваю ни один из параметров element, compiler, **kwargs, что делает меня немного подозрительным.

Я только что приготовил это, поработав около 1,5 дней с SA & PG, так что вполне возможно, что еще есть возможности для улучшения. Я был бы очень признателен за любые замечания w.r.t. этот подход. Спасибо.

0
hsk81 24 Янв 2013 в 15:54

Я включил отзыв zzzeek от его https://gist.github.com/4625858 ради полноты:

from sqlalchemy.sql.expression import ColumnElement ## !!
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.compiler import compiles
from sqlalchemy import inspect

class UuidPathColumn (ColumnElement):
    def __init__(self, entity):
        insp = inspect (entity)
        self.entity = insp.selectable

@compiles (UuidPathColumn)
def compile_uuid_path_column (element, compiler, **kwargs):
    return "%s.uuid_path" % compiler.process (element.entity, ashint=True)

class NamePathColumn (ColumnElement):
    def __init__(self, entity):
        insp = inspect (entity)
        self.entity = insp.selectable

@compiles (NamePathColumn)
def compile_name_path_column (element, compiler, **kwargs):
    return "%s.name_path" % compiler.process (element.entity, ashint=True)

Для этого важно использовать ColumnElement (а не ColumnClause); соответствующий код можно найти по адресу node.py, name_path и uuid_path. Этот материал был реализован с помощью SQLAlchemy 0.8 и PostgreSQL 9.2.

0
hsk81 24 Янв 2013 в 19:02