Доверенность ассоциации

associationproxy используется для создания представления чтения/записи целевого атрибута в рамках отношения. По сути, он скрывает использование «среднего» атрибута между двумя конечными точками, и может использоваться для отбора полей из коллекции связанных объектов или для уменьшения многословности использования шаблона объекта ассоциации. При творческом применении ассоциативный прокси позволяет создавать сложные коллекции и словарные представления практически любой геометрии, сохраняемые в базе данных с помощью стандартных, прозрачно настроенных реляционных шаблонов.

Упрощение скалярных коллекций

Рассмотрим отображение многие-ко-многим между двумя классами, User и Keyword. Каждый User может иметь любое количество объектов Keyword, и наоборот (модель «многие-ко-многим» описана в Многие ко многим):

from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import declarative_base, relationship

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))
    kw = relationship("Keyword", secondary=lambda: user_keyword_table)

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


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

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


user_keyword_table = Table(
    "user_keyword",
    Base.metadata,
    Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
    Column("keyword_id", Integer, ForeignKey("keyword.id"), primary_key=True),
)

Чтение и манипулирование коллекцией строк «ключевых слов», связанных с User, требует обхода от каждого элемента коллекции до атрибута .keyword, что может быть неудобно:

>>> user = User("jek")
>>> user.kw.append(Keyword("cheese-inspector"))
>>> print(user.kw)
[<__main__.Keyword object at 0x12bf830>]
>>> print(user.kw[0].keyword)
cheese-inspector
>>> print([keyword.keyword for keyword in user.kw])
['cheese-inspector']

association_proxy применяется к классу User для создания «представления» отношения kw, которое раскрывает только строковое значение .keyword, связанное с каждым объектом Keyword:

from sqlalchemy.ext.associationproxy import association_proxy


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))
    kw = relationship("Keyword", secondary=lambda: user_keyword_table)

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

    # proxy the 'keyword' attribute from the 'kw' relationship
    keywords = association_proxy("kw", "keyword")

Теперь мы можем ссылаться на коллекцию .keywords как на листинг строк, который можно как читать, так и записывать. Новые объекты Keyword создаются для нас прозрачно:

>>> user = User("jek")
>>> user.keywords.append("cheese-inspector")
>>> user.keywords
['cheese-inspector']
>>> user.keywords.append("snack ninja")
>>> user.kw
[<__main__.Keyword object at 0x12cdd30>, <__main__.Keyword object at 0x12cde30>]

Объект AssociationProxy, создаваемый функцией association_proxy(), является экземпляром Python descriptor. Он всегда объявляется вместе с отображаемым пользовательским классом, независимо от того, используется ли декларативное или классическое отображение с помощью функции mapper().

Прокси функционирует, оперируя базовым отображаемым атрибутом или коллекцией в ответ на операции, и изменения, сделанные через прокси, немедленно отображаются в отображаемом атрибуте, а также наоборот. Базовый атрибут остается полностью доступным.

При первом обращении ассоциативный прокси выполняет операции интроспекции целевой коллекции, чтобы ее поведение соответствовало действительности. Учитываются такие детали, как является ли локально проксируемый атрибут коллекцией (что типично) или скалярной ссылкой, а также действует ли коллекция как набор, список или словарь, так что прокси должен действовать так же, как и базовая коллекция или атрибут.

Создание новых ценностей

Когда событие list append() (или set add(), dictionary __setitem__(), или scalar assignment event) перехватывается прокси ассоциацией, она создает новый экземпляр «промежуточного» объекта, используя его конструктор, передавая в качестве единственного аргумента заданное значение. В нашем примере выше, операция типа:

user.keywords.append("cheese-inspector")

Переводится прокси ассоциацией в операцию:

user.kw.append(Keyword("cheese-inspector"))

Пример работает, потому что мы разработали конструктор для Keyword, который принимает один позиционный аргумент keyword. Для тех случаев, когда конструктор с одним аргументом не подходит, поведение прокси ассоциации при создании можно настроить с помощью аргумента creator, который ссылается на вызываемый объект (т.е. функцию Python), который будет создавать новый экземпляр объекта, учитывая единственный аргумент. Ниже мы проиллюстрируем это с помощью типичной лямбды:

class User(Base):
    # ...

    # use Keyword(keyword=kw) on append() events
    keywords = association_proxy(
        "kw", "keyword", creator=lambda kw: Keyword(keyword=kw)
    )

Функция creator принимает один аргумент в случае коллекции, основанной на списке или наборе, или скалярный атрибут. В случае коллекции на основе словаря она принимает два аргумента, «ключ» и «значение». Пример этого приведен ниже в Проксирование к коллекциям на основе словарей.

Упрощение объектов ассоциации

Шаблон «объект ассоциации» является расширенной формой отношения «многие-ко-многим» и описан в Объект ассоциации. Прокси ассоциации полезны для того, чтобы не мешать «объектам ассоциации» при регулярном использовании.

Предположим, что наша таблица user_keyword имеет дополнительные столбцы, которые мы хотели бы отобразить явно, но в большинстве случаев нам не требуется прямой доступ к этим атрибутам. Ниже мы проиллюстрируем новое отображение, которое представляет класс UserKeywordAssociation, который отображается на таблицу user_keyword, показанную ранее. Этот класс добавляет дополнительный столбец special_key, значение, к которому мы иногда хотим получить доступ, но не в обычном случае. Мы создаем ассоциативный прокси на классе User под названием keywords, который будет мостом между коллекцией user_keyword_associations User и атрибутом .keyword, присутствующим на каждом UserKeywordAssociation:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import declarative_base, relationship

Base = declarative_base()


class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    user_keyword_associations = relationship(
        "UserKeywordAssociation",
        back_populates="user",
        cascade="all, delete-orphan",
    )
    # association proxy of "user_keyword_associations" collection
    # to "keyword" attribute
    keywords = association_proxy("user_keyword_associations", "keyword")

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


class UserKeywordAssociation(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)
    special_key = Column(String(50))

    user = relationship(User, back_populates="user_keyword_associations")

    # reference to the "Keyword" object
    keyword = relationship("Keyword")

    def __init__(self, keyword=None, user=None, special_key=None):
        self.user = user
        self.keyword = keyword
        self.special_key = special_key


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

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

    def __repr__(self):
        return "Keyword(%s)" % repr(self.keyword)

С помощью приведенной выше конфигурации мы можем работать с коллекцией .keywords каждого объекта User, каждый из которых раскрывает коллекцию объектов Keyword, полученных из базовых элементов UserKeywordAssociation:

>>> user = User("log")
>>> for kw in (Keyword("new_from_blammo"), Keyword("its_big")):
...     user.keywords.append(kw)
>>> print(user.keywords)
[Keyword('new_from_blammo'), Keyword('its_big')]

Этот пример отличается от примера, показанного ранее в Упрощение скалярных коллекций, где ассоциативный прокси раскрывал коллекцию строк, а не коллекцию составленных объектов. В этом случае каждая операция .keywords.append() эквивалентна:

>>> user.user_keyword_associations.append(UserKeywordAssociation(Keyword("its_heavy")))

Объект UserKeywordAssociation имеет два атрибута, которые оба заполняются в рамках операции append() прокси ассоциации; .keyword, который ссылается на Keyword` object, and ``.user, which refers to the User. The .keyword attribute is populated first, as the association proxy generates a new UserKeywordAssociation object in response to the .append() operation, assigning the given Keyword instance to the . keyword attribute. Then, as the UserKeywordAssociation object is appended to the User.user_keyword_associations collection, the UserKeywordAssociation.user attribute, configured as back_populates for User.user_keyword_associations, is initialized upon the given UserKeywordAssociation instance to refer to the parent User receiving the append operation. The special_key argument above is left at its default value of None.

Для тех случаев, когда мы хотим, чтобы special_key имел значение, мы создаем объект UserKeywordAssociation явным образом. Ниже мы присваиваем все три атрибута, при этом присвоение .user при построении имеет эффект добавления нового UserKeywordAssociation к коллекции User.user_keyword_associations (через отношение):

>>> UserKeywordAssociation(Keyword("its_wood"), user, special_key="my special key")

Прокси ассоциации возвращает нам коллекцию объектов Keyword, представленных всеми этими операциями:

>>> user.keywords
[Keyword('new_from_blammo'), Keyword('its_big'), Keyword('its_heavy'), Keyword('its_wood')]

Проксирование к коллекциям на основе словарей

Ассоциативный прокси может также проксировать коллекции на основе словарей. В связках SQLAlchemy обычно используется тип коллекции attribute_mapped_collection() для создания словарных коллекций, а также расширенные методы, описанные в Пользовательские коллекции на основе словарей.

Прокси ассоциации корректирует свое поведение, когда обнаруживает использование коллекции на основе словаря. Когда в словарь добавляются новые значения, прокси ассоциации инстанцирует объект-посредник, передавая функции создания два аргумента вместо одного - ключ и значение. Как всегда, эта функция создания по умолчанию использует конструктор класса-посредника, и может быть настроена с помощью аргумента creator.

Ниже мы изменим наш пример UserKeywordAssociation таким образом, что коллекция User.user_keyword_associations теперь будет отображаться с помощью словаря, где аргумент UserKeywordAssociation.special_key будет использоваться в качестве ключа для словаря. Мы также применяем аргумент creator к прокси User.keywords, чтобы эти значения присваивались соответствующим образом при добавлении новых элементов в словарь:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    # user/user_keyword_associations relationship, mapping
    # user_keyword_associations with a dictionary against "special_key" as key.
    user_keyword_associations = relationship(
        "UserKeywordAssociation",
        back_populates="user",
        collection_class=attribute_mapped_collection("special_key"),
        cascade="all, delete-orphan",
    )
    # proxy to 'user_keyword_associations', instantiating
    # UserKeywordAssociation assigning the new key to 'special_key',
    # values to 'keyword'.
    keywords = association_proxy(
        "user_keyword_associations",
        "keyword",
        creator=lambda k, v: UserKeywordAssociation(special_key=k, keyword=v),
    )

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


class UserKeywordAssociation(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)
    special_key = Column(String)

    user = relationship(
        User,
        back_populates="user_keyword_associations",
    )
    keyword = relationship("Keyword")


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

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

    def __repr__(self):
        return "Keyword(%s)" % repr(self.keyword)

Мы иллюстрируем коллекцию .keywords как словарь, отображая значение UserKeywordAssociation.special_key на объекты Keyword:

>>> user = User("log")

>>> user.keywords["sk1"] = Keyword("kw1")
>>> user.keywords["sk2"] = Keyword("kw2")

>>> print(user.keywords)
{'sk1': Keyword('kw1'), 'sk2': Keyword('kw2')}

Доверенности объединений

Учитывая наши предыдущие примеры проксирования от отношения к скалярному атрибуту, проксирования через объект ассоциации и проксирования словарей, мы можем объединить все три техники вместе, чтобы дать User словарь keywords, который имеет дело строго со строковым значением special_key, отображенным на строку keyword. Оба класса UserKeywordAssociation и Keyword полностью скрыты. Это достигается путем создания ассоциативного прокси на User, который ссылается на ассоциативный прокси, присутствующий на UserKeywordAssociation:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    user_keyword_associations = relationship(
        "UserKeywordAssociation",
        back_populates="user",
        collection_class=attribute_mapped_collection("special_key"),
        cascade="all, delete-orphan",
    )
    # the same 'user_keyword_associations'->'keyword' proxy as in
    # the basic dictionary example.
    keywords = association_proxy(
        "user_keyword_associations",
        "keyword",
        creator=lambda k, v: UserKeywordAssociation(special_key=k, keyword=v),
    )

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


class UserKeywordAssociation(Base):
    __tablename__ = "user_keyword"
    user_id = Column(ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(ForeignKey("keyword.id"), primary_key=True)
    special_key = Column(String)
    user = relationship(
        User,
        back_populates="user_keyword_associations",
    )

    # the relationship to Keyword is now called
    # 'kw'
    kw = relationship("Keyword")

    # 'keyword' is changed to be a proxy to the
    # 'keyword' attribute of 'Keyword'
    keyword = association_proxy("kw", "keyword")


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

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

User.keywords теперь является словарем string to string, где объекты UserKeywordAssociation и Keyword создаются и удаляются для нас прозрачно с помощью прокси ассоциации. В примере ниже мы иллюстрируем использование оператора присваивания, также соответствующим образом обрабатываемого прокси ассоциации, для одновременного применения словарного значения к коллекции:

>>> user = User("log")
>>> user.keywords = {"sk1": "kw1", "sk2": "kw2"}
>>> print(user.keywords)
{'sk1': 'kw1', 'sk2': 'kw2'}

>>> user.keywords["sk3"] = "kw3"
>>> del user.keywords["sk2"]
>>> print(user.keywords)
{'sk1': 'kw1', 'sk3': 'kw3'}

>>> # illustrate un-proxied usage
... print(user.user_keyword_associations["sk3"].kw)
<__main__.Keyword object at 0x12ceb90>

Одна оговорка в нашем примере выше заключается в том, что поскольку объекты Keyword создаются для каждой операции набора словаря, в примере не удается сохранить уникальность объектов Keyword по их строковому имени, что является типичным требованием для сценария тегирования, подобного этому. Для этого случая рекомендуется рецепт UniqueObject или аналогичная стратегия создания, которая будет применять стратегию «сначала искать, потом создавать» к конструктору класса Keyword, так что уже существующий Keyword будет возвращен, если заданное имя уже присутствует.

Запрос с использованием прокси ассоциаций

AssociationProxy имеет простые возможности построения SQL, которые работают на уровне класса аналогично другим атрибутам ORM-mapped, и обеспечивают рудиментарную поддержку фильтрации, основанную в основном на ключевом слове SQL EXISTS.

Примечание

Основная цель расширения ассоциативного прокси заключается в обеспечении улучшенной персистентности и моделей доступа к объектам с уже загруженными сопоставленными экземплярами объектов. Функция запросов с привязкой к классу имеет ограниченное применение и не заменит необходимость обращения к базовым атрибутам при построении SQL-запросов с JOINами, опциями ускоренной загрузки и т.д.

Для этого раздела рассмотрим класс, в котором есть как ассоциативный прокси, ссылающийся на столбец, так и ассоциативный прокси, ссылающийся на связанный объект, как в приведенном ниже примере отображения:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.collections import attribute_mapped_collection

Base = declarative_base()


class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    user_keyword_associations = relationship(
        "UserKeywordAssociation",
        cascade="all, delete-orphan",
    )

    # object-targeted association proxy
    keywords = association_proxy(
        "user_keyword_associations",
        "keyword",
    )

    # column-targeted association proxy
    special_keys = association_proxy("user_keyword_associations", "special_key")


class UserKeywordAssociation(Base):
    __tablename__ = "user_keyword"
    user_id = Column(ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(ForeignKey("keyword.id"), primary_key=True)
    special_key = Column(String)
    keyword = relationship("Keyword")


class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

Генерируемый SQL принимает форму коррелированного подзапроса к SQL-оператору EXISTS, поэтому его можно использовать в предложении WHERE без необходимости дополнительных модификаций вложенного запроса. Если непосредственной целью ассоциативного прокси является выражение сопоставленного столбца, можно использовать стандартные операторы столбцов, которые будут встроены в подзапрос. Например, прямой оператор равенства:

>>> print(session.query(User).filter(User.special_keys == "jek"))
SELECT "user".id AS user_id, "user".name AS user_name
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_keyword
WHERE "user".id = user_keyword.user_id AND user_keyword.special_key = :special_key_1)

оператор LIKE:

>>> print(session.query(User).filter(User.special_keys.like("%jek")))
SELECT "user".id AS user_id, "user".name AS user_name
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_keyword
WHERE "user".id = user_keyword.user_id AND user_keyword.special_key LIKE :special_key_1)

Для ассоциативных прокси, где непосредственной целью является связанный объект или коллекция, или другой ассоциативный прокси или атрибут на связанном объекте, вместо них можно использовать операторы, ориентированные на отношения, такие как PropComparator.has() и PropComparator.any(). Атрибут User.keywords фактически является двумя ассоциативными прокси, связанными вместе, поэтому при использовании этого прокси для генерации фраз SQL мы получаем два уровня подзапросов EXISTS:

>>> print(session.query(User).filter(User.keywords.any(Keyword.keyword == "jek")))
SELECT "user".id AS user_id, "user".name AS user_name
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_keyword
WHERE "user".id = user_keyword.user_id AND (EXISTS (SELECT 1
FROM keyword
WHERE keyword.id = user_keyword.keyword_id AND keyword.keyword = :keyword_1)))

Это не самая эффективная форма SQL, поэтому, хотя ассоциативные прокси могут быть удобны для быстрого создания критериев WHERE, результаты SQL должны быть проверены и «развернуты» в явные критерии JOIN для наилучшего использования, особенно при объединении ассоциативных прокси вместе.

Изменено в версии 1.3: Ассоциативный прокси имеет различные режимы запросов в зависимости от типа цели. См. AssociationProxy теперь предоставляет стандартные операторы столбцов для целей, ориентированных на столбцы.

Каскадные скалярные удаления

Добавлено в версии 1.3.

Дано отображение в виде:

class A(Base):
    __tablename__ = "test_a"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="a", uselist=False)
    b = association_proxy(
        "ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
    )


class B(Base):
    __tablename__ = "test_b"
    id = Column(Integer, primary_key=True)
    ab = relationship("AB", backref="b", cascade="all, delete-orphan")


class AB(Base):
    __tablename__ = "test_ab"
    a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
    b_id = Column(Integer, ForeignKey(B.id), primary_key=True)

Присвоение A.b порождает объект AB:

a.b = B()

Ассоциация A.b является скалярной и включает использование флага AssociationProxy.cascade_scalar_deletes. Если флаг установлен, установка A.b в None удалит и A.ab:

a.b = None
assert a.ab is None

Когда AssociationProxy.cascade_scalar_deletes не установлен, объект ассоциации a.ab, описанный выше, останется на месте.

Обратите внимание, что такое поведение не характерно для прокси ассоциаций на основе коллекции; в этом случае объект промежуточной ассоциации всегда удаляется, когда удаляются члены проксируемой коллекции. Удаление строки зависит от настройки каскада отношений.

См.также

Каскады

Документация API

Вернуться на верх