Конфигурация и техника инкассации

Функция relationship() определяет связь между двумя классами. Если связь определяет отношения «один ко многим» или «многие ко многим», она представляется как коллекция Python при загрузке и манипулировании объектами. В этом разделе представлена дополнительная информация о конфигурации и методах работы с коллекциями.

Работа с большими коллекциями

Поведение relationship() по умолчанию заключается в полной загрузке коллекции элементов внутрь, в соответствии со стратегией загрузки отношения. Кроме того, Session по умолчанию умеет удалять только те объекты, которые действительно присутствуют в сессии. Когда родительский экземпляр помечен на удаление и удален, Session загружает полный список дочерних элементов, чтобы они также были удалены или их значение внешнего ключа было установлено на null; это делается для того, чтобы избежать нарушения ограничений. Для больших коллекций дочерних элементов существует несколько стратегий, позволяющих обойти полную загрузку дочерних элементов как во время загрузки, так и во время удаления.

Динамические загрузчики отношений

Примечание

SQLAlchemy 2.0 будет иметь немного измененный шаблон для «динамических» загрузчиков, который не будет полагаться на объект Query, который станет наследием в 2.0. О текущих стратегиях миграции смотрите Использование «динамической» загрузки отношений без использования Query.

Примечание

Этот загрузчик в общем случае не совместим с расширением Асинхронный ввод/вывод (asyncio). Его можно использовать с некоторыми ограничениями, как указано в Asyncio dynamic guidelines.

Отношение relationship(), соответствующее большой коллекции, может быть настроено так, чтобы при обращении к нему возвращался унаследованный объект Query, что позволяет фильтровать отношения по критериям. Класс - это специальный класс AppenderQuery, возвращаемый вместо коллекции при обращении к нему. Может применяться критерий фильтрации, а также ограничения и смещения, явно или через срезы массива:

class User(Base):
    __tablename__ = "user"

    posts = relationship(Post, lazy="dynamic")


jack = session.get(User, id)

# filter Jack's blog posts
posts = jack.posts.filter(Post.headline == "this is a post")

# apply array slices
posts = jack.posts[5:20]

Динамические отношения поддерживают ограниченные операции записи с помощью методов AppenderQuery.append() и AppenderQuery.remove():

oldpost = jack.posts.filter(Post.headline == "old post").one()
jack.posts.remove(oldpost)

jack.posts.append(Post("new post"))

Поскольку сторона чтения динамического отношения всегда запрашивает базу данных, изменения в базовой коллекции не будут видны до тех пор, пока данные не будут удалены. Однако, если на используемом Session включена функция «autoflush», это будет происходить автоматически каждый раз, когда коллекция собирается выполнить запрос.

Чтобы поместить динамическое отношение на обратную ссылку, используйте функцию backref() в сочетании с lazy='dynamic':

class Post(Base):
    __table__ = posts_table

    user = relationship(User, backref=backref("posts", lazy="dynamic"))

Обратите внимание, что опции eager/lazy loading не могут быть использованы в сочетании с динамическими отношениями в настоящее время.

Примечание

Функция dynamic_loader() по сути аналогична relationship() с указанным аргументом lazy='dynamic'.

Предупреждение

Динамический» загрузчик применяется только к коллекциям. Недопустимо использовать «динамические» загрузчики с отношениями «многие-к-одному», «один-к-одному» или uselist=False. Новые версии SQLAlchemy выдают предупреждения или исключения в таких случаях.

Установки Noload, RaiseLoad

Отношения «noload» никогда не загружаются из базы данных, даже при обращении к ним. Оно настраивается с помощью lazy='noload':

class MyClass(Base):
    __tablename__ = "some_table"

    children = relationship(MyOtherClass, lazy="noload")

Выше, коллекция children полностью доступна для записи, и изменения в ней будут сохранены в базе данных, а также доступны для чтения локально в момент их добавления. Однако когда экземпляры MyClass только что загружены из базы данных, коллекция children остается пустой. Стратегия noload также доступна на основе опции запроса с помощью опции загрузчика noload().

В качестве альтернативы, отношение «raise»-loaded будет вызывать InvalidRequestError там, где атрибут обычно выдает ленивую загрузку:

class MyClass(Base):
    __tablename__ = "some_table"

    children = relationship(MyOtherClass, lazy="raise")

Выше, доступ к атрибутам коллекции children вызовет исключение, если она не была предварительно загружена. Это относится к доступу на чтение, но для коллекций также повлияет на доступ на запись, поскольку коллекции не могут быть изменены без предварительной загрузки. Это делается для того, чтобы убедиться, что приложение не создает неожиданных ленивых загрузок в определенном контексте. Вместо того, чтобы читать журналы SQL, чтобы определить, что все необходимые атрибуты были загружены с нетерпением, стратегия «raise» заставит незагруженные атрибуты немедленно подниматься при обращении к ним. Стратегия «raise» также доступна на основе опции запроса с помощью опции загрузчика raiseload().

Добавлено в версии 1.1: добавлена стратегия «поднять» загрузчика.

Использование пассивных удалений

См. раздел Использование каскада внешних ключей ON DELETE с отношениями ORM для этого раздела.

Настройка доступа к коллекции

Сопоставление отношений один-ко-многим или многие-ко-многим приводит к созданию коллекции значений, доступных через атрибут родительского экземпляра. По умолчанию эта коллекция представляет собой list:

class Parent(Base):
    __tablename__ = "parent"
    parent_id = Column(Integer, primary_key=True)

    children = relationship(Child)


parent = Parent()
parent.children.append(Child())
print(parent.children[0])

Коллекции не ограничиваются списками. Наборы, изменяемые последовательности и практически любой другой объект Python, который может выступать в качестве контейнера, можно использовать вместо списка по умолчанию, указав опцию relationship.collection_class на relationship():

class Parent(Base):
    __tablename__ = "parent"
    parent_id = Column(Integer, primary_key=True)

    # use a set
    children = relationship(Child, collection_class=set)


parent = Parent()
child = Child()
parent.children.add(child)
assert child in parent.children

Коллекции словарей

При использовании словаря в качестве коллекции требуется небольшая дополнительная деталь. Это связано с тем, что объекты всегда загружаются из базы данных в виде списков, и для правильного заполнения словаря необходимо иметь стратегию генерации ключей. Функция attribute_mapped_collection() является наиболее распространенным способом создания простой коллекции словарей. Она создает класс словаря, который будет применять определенный атрибут сопоставленного класса в качестве ключа. Ниже мы отображаем класс Item, содержащий словарь из Note элементов, ключом к которому служит атрибут Note.keyword:

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

Base = declarative_base()


class Item(Base):
    __tablename__ = "item"
    id = Column(Integer, primary_key=True)
    notes = relationship(
        "Note",
        collection_class=attribute_mapped_collection("keyword"),
        cascade="all, delete-orphan",
    )


class Note(Base):
    __tablename__ = "note"
    id = Column(Integer, primary_key=True)
    item_id = Column(Integer, ForeignKey("item.id"), nullable=False)
    keyword = Column(String)
    text = Column(String)

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

Item.notes является словарем:

>>> item = Item()
>>> item.notes["a"] = Note("a", "atext")
>>> item.notes.items()
{'a': <__main__.Note object at 0x2eaaf0>}

attribute_mapped_collection() будет гарантировать, что атрибут .keyword каждого Note соответствует ключу в словаре. Например, при назначении на Item.notes, ключ словаря, который мы предоставляем, должен соответствовать ключу фактического объекта Note:

item = Item()
item.notes = {
    "a": Note("a", "atext"),
    "b": Note("b", "btext"),
}

Атрибут, который attribute_mapped_collection() использует в качестве ключа, вообще не нужно сопоставлять! Использование обычного Python @property позволяет использовать в качестве ключа практически любую деталь или комбинацию деталей об объекте, как показано ниже, когда мы задаем его как кортеж из Note.keyword и первых десяти букв поля Note.text:

class Item(Base):
    __tablename__ = "item"
    id = Column(Integer, primary_key=True)
    notes = relationship(
        "Note",
        collection_class=attribute_mapped_collection("note_key"),
        backref="item",
        cascade="all, delete-orphan",
    )


class Note(Base):
    __tablename__ = "note"
    id = Column(Integer, primary_key=True)
    item_id = Column(Integer, ForeignKey("item.id"), nullable=False)
    keyword = Column(String)
    text = Column(String)

    @property
    def note_key(self):
        return (self.keyword, self.text[0:10])

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

Выше мы добавили обратную ссылку Note.item. Присваивая этому обратное отношение, Note добавляется в словарь Item.notes и ключ генерируется для нас автоматически:

>>> item = Item()
>>> n1 = Note("a", "atext")
>>> n1.item = item
>>> item.notes
{('a', 'atext'): <__main__.Note object at 0x2eaaf0>}

Другие встроенные типы словарей включают column_mapped_collection(), который почти аналогичен attribute_mapped_collection(), за исключением того, что объект Column передается непосредственно:

from sqlalchemy.orm.collections import column_mapped_collection


class Item(Base):
    __tablename__ = "item"
    id = Column(Integer, primary_key=True)
    notes = relationship(
        "Note",
        collection_class=column_mapped_collection(Note.__table__.c.keyword),
        cascade="all, delete-orphan",
    )

а также mapped_collection(), которому передается любая вызываемая функция. Обратите внимание, что обычно проще использовать attribute_mapped_collection() вместе с @property, как упоминалось ранее:

from sqlalchemy.orm.collections import mapped_collection


class Item(Base):
    __tablename__ = "item"
    id = Column(Integer, primary_key=True)
    notes = relationship(
        "Note",
        collection_class=mapped_collection(lambda note: note.text[0:10]),
        cascade="all, delete-orphan",
    )

Сопоставления словарей часто комбинируются с расширением «Association Proxy» для создания упрощенных представлений словарей. Примеры смотрите в Проксирование к коллекциям на основе словарей и Доверенности объединений.

Работа с мутациями ключей и обратное наполнение для коллекций словарей

При использовании attribute_mapped_collection() «ключ» для словаря берется из атрибута целевого объекта. Изменения этого ключа не отслеживаются. Это означает, что ключ должен быть назначен при первом использовании, и если ключ изменится, коллекция не будет изменена. Типичный пример, когда это может стать проблемой, - это использование обратных ссылок для заполнения коллекции, сопоставленной с атрибутами. Учитывая следующее:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship(
        "B",
        collection_class=attribute_mapped_collection("data"),
        back_populates="a",
    )


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    data = Column(String)

    a = relationship("A", back_populates="bs")

Выше, если мы создадим B(), который ссылается на определенный A(), то обратное заполнение добавит B() в коллекцию A.bs, однако если значение B.data еще не установлено, то ключом будет None:

>>> a1 = A()
>>> b1 = B(a=a1)
>>> a1.bs
{None: <test3.B object at 0x7f7b1023ef70>}

Установка b1.data после факта не обновляет коллекцию:

>>> b1.data = "the key"
>>> a1.bs
{None: <test3.B object at 0x7f7b1023ef70>}

Это также можно увидеть, если попытаться задать B() в конструкторе. Порядок аргументов меняет результат:

>>> B(a=a1, data="the key")
<test3.B object at 0x7f7b10114280>
>>> a1.bs
{None: <test3.B object at 0x7f7b10114280>}

против:

>>> B(data="the key", a=a1)
<test3.B object at 0x7f7b10114340>
>>> a1.bs
{'the key': <test3.B object at 0x7f7b10114340>}

Если обратные ссылки используются таким образом, убедитесь, что атрибуты заполняются в правильном порядке, используя метод __init__.

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

from sqlalchemy import event
from sqlalchemy.orm import attributes


@event.listens_for(B.data, "set")
def set_item(obj, value, previous, initiator):
    if obj.a is not None:
        previous = None if previous == attributes.NO_VALUE else previous
        obj.a.bs[value] = obj
        obj.a.bs.pop(previous)

Реализация пользовательских коллекций

Вы также можете использовать собственные типы для коллекций. В простых случаях достаточно наследовать от list или set, добавив пользовательское поведение. В других случаях необходимы специальные декораторы, чтобы сообщить SQLAlchemy более подробную информацию о том, как работает коллекция.

Коллекции в SQLAlchemy прозрачно инструментируются. Инструментирование означает, что обычные операции над коллекцией отслеживаются и приводят к записи изменений в базу данных во время вспышки. Кроме того, операции над коллекцией могут вызывать события, которые указывают на необходимость выполнения некоторой вторичной операции. Примеры вторичных операций включают сохранение дочернего элемента в родительском Session (т.е. каскад save-update), а также синхронизацию состояния двунаправленных отношений (т.е. backref()).

Пакет collections понимает базовый интерфейс списков, множеств и dicts и автоматически применяет инструментарий к этим встроенным типам и их подклассам. Производные типы объектов, реализующие базовый интерфейс коллекций, обнаруживаются и инструментируются с помощью duck-typing:

class ListLike(object):
    def __init__(self):
        self.data = []

    def append(self, item):
        self.data.append(item)

    def remove(self, item):
        self.data.remove(item)

    def extend(self, items):
        self.data.extend(items)

    def __iter__(self):
        return iter(self.data)

    def foo(self):
        return "foo"

append, remove и extend являются известными спископодобными методами и будут инструментированы автоматически. __iter__ не является методом-мутатором и не будет инструментироваться, и foo тоже не будет.

Утиная типизация (т.е. догадки), конечно, не является надежной, поэтому вы можете явно указать интерфейс, который вы реализуете, предоставив атрибут класса __emulates__:

class SetLike(object):
    __emulates__ = set

    def __init__(self):
        self.data = set()

    def append(self, item):
        self.data.add(item)

    def remove(self, item):
        self.data.remove(item)

    def __iter__(self):
        return iter(self.data)

Этот класс выглядит спископодобным из-за append, но __emulates__ заставляет его быть set-подобным. Известно, что remove является частью интерфейса set и будет подвергнут инструментальной проверке.

Но этот класс еще не работает: необходимо немного подправить его для использования в SQLAlchemy. ORM должен знать, какие методы использовать для добавления, удаления и итерации членов коллекции. При использовании таких типов, как list или set, соответствующие методы хорошо известны и используются автоматически при их наличии. Этот класс типа set не предоставляет ожидаемого метода add, поэтому мы должны предоставить явное отображение для ORM с помощью декоратора.

Аннотирование пользовательских коллекций с помощью декораторов

Декораторы можно использовать для обозначения отдельных методов, необходимых ORM для управления коллекциями. Используйте их, когда ваш класс не совсем соответствует обычному интерфейсу для своего типа контейнера, или когда вы хотите использовать другой метод для выполнения работы.

from sqlalchemy.orm.collections import collection


class SetLike(object):
    __emulates__ = set

    def __init__(self):
        self.data = set()

    @collection.appender
    def append(self, item):
        self.data.add(item)

    def remove(self, item):
        self.data.remove(item)

    def __iter__(self):
        return iter(self.data)

И это все, что необходимо для завершения примера. SQLAlchemy будет добавлять экземпляры с помощью метода append. remove и __iter__ являются методами по умолчанию для множеств и будут использоваться для удаления и итерации. Методы по умолчанию могут быть изменены:

from sqlalchemy.orm.collections import collection

class MyList(list):
    @collection.remover
    def zark(self, item):
        # do something special...

    @collection.iterator
    def hey_use_this_instead_for_iteration(self):
        # ...

Не требуется, чтобы коллекция была списком или набором. Классы коллекций могут быть любой формы, лишь бы они имели интерфейс append, remove и iterate, обозначенный для использования SQLAlchemy. Методы append и remove будут вызываться с сопоставленной сущностью в качестве единственного аргумента, а методы iterator вызываются без аргументов и должны возвращать итератор.

Пользовательские коллекции на основе словарей

Класс MappedCollection можно использовать в качестве базового класса для ваших пользовательских типов или как микс-ин для быстрого добавления поддержки коллекции dict в другие классы. Он использует ключевую функцию для делегирования функций __setitem__ и __delitem__:

from sqlalchemy.util import OrderedDict
from sqlalchemy.orm.collections import MappedCollection


class NodeMap(OrderedDict, MappedCollection):
    """Holds 'Node' objects, keyed by the 'name' attribute with insert order maintained."""

    def __init__(self, *args, **kw):
        MappedCollection.__init__(self, keyfunc=lambda node: node.name)
        OrderedDict.__init__(self, *args, **kw)

При подклассификации MappedCollection, пользовательские версии __setitem__() или __delitem__() должны быть украшены collection.internally_instrumented(), если они вызывают те же методы на MappedCollection. Это связано с тем, что методы на MappedCollection уже инструментированы - их вызов из уже инструментированного вызова может вызвать повторное или неуместное срабатывание событий, что в редких случаях приводит к повреждению внутреннего состояния:

from sqlalchemy.orm.collections import MappedCollection, collection


class MyMappedCollection(MappedCollection):
    """Use @internally_instrumented when your methods
    call down to already-instrumented methods.

    """

    @collection.internally_instrumented
    def __setitem__(self, key, value, _sa_initiator=None):
        # do something with key, value
        super(MyMappedCollection, self).__setitem__(key, value, _sa_initiator)

    @collection.internally_instrumented
    def __delitem__(self, key, _sa_initiator=None):
        # do something with key
        super(MyMappedCollection, self).__delitem__(key, _sa_initiator)

ORM понимает интерфейс dict так же, как списки и множества, и будет автоматически использовать все диктоподобные методы, если вы решите подклассифицировать dict или обеспечить диктоподобное поведение коллекции в классе с типом «утка». Однако вы должны украсить методы appender и remover - в базовом интерфейсе словаря нет совместимых методов, которые SQLAlchemy использовал бы по умолчанию. Итерация будет проходить через itervalues(), если не оформлено иначе.

Инструментарий и пользовательские типы

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

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

class MyAwesomeList(some.great.library.AwesomeList):
    pass


# ... relationship(..., collection_class=MyAwesomeList)

ORM использует этот подход для встроенных элементов, спокойно подставляя тривиальный подкласс, когда list, set или dict используется напрямую.

Внутренние компоненты коллекции

Различные внутренние методы.

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