Spec-Zone.ru › Phoenix

Самоссылочная связь «многие ко многим»

Исходный код

Ecto.Schema.many_to_many/3 используется для установления связи между двумя схемами с помощью таблицы соединения (или схемы соединения), которая отслеживает связь между ними. Но что, если мы хотим, чтобы таблица ссылалась сама на себя? Это часто используется для симметричных отношений и обычно называется самоссылочной ассоциацией many_to_many.

Отношения между людьми

Представим, что мы создаём систему, поддерживающую модель отношений между людьми.

defmodule MyApp.Accounts.Person do
  use Ecto.Schema

  alias MyApp.Accounts.Person
  alias MyApp.Relationships.Relationship

  schema "people" do
    field :name, :string

    many_to_many :relationships,
                 Person,
                 join_through: Relationship,
                 join_keys: [person_id: :id, relation_id: :id]

    many_to_many :reverse_relationships,
                 Person,
                 join_through: Relationship,
                 join_keys: [relation_id: :id, person_id: :id]

    timestamps()
  end
end

defmodule MyApp.Relationships.Relationship do
  use Ecto.Schema

  schema "relationships" do
    field :person_id, :id
    field :relation_id, :id
    timestamps()
  end
end

В нашем примере мы реализуем промежуточную схему, MyApp.Relationships.Relationship, для параметра :join_through и передаём пару идентификаторов, для которых создадим уникальный индекс в миграции базы данных. Промежуточная схема позволяет легко добавлять дополнительные атрибуты и функциональность к отношениям в будущем.

Чтобы завершить вторую половину ассоциации, нам пришлось добавить ещё один вызов many_to_many :reverse_relationships с обратным значением :join_keys. Это гарантирует, что обе стороны отношения будут добавлены в базу данных, когда одна из сторон успешно завершит запрос на установление отношения.

У человека, являющегося обратной стороной отношения, структура отношения будет храниться в списке под ключом "reverse_relationships". Затем мы можем построить запросы как для :relationships, так и для :reverse_relationships с правильным :preload:

iex> preloads = [:relationships, :reverse_relationships]
iex> people = Repo.all from p in Person, preload: preloads
[
  MyApp.Accounts.Person<
    ...
    relationships: [
      MyApp.Accounts.Person<
        id: ...,
        ...
      >
    ]
  >,
  MyApp.Accounts.Person<
    ...
    reverse_relationships: [
      MyApp.Accounts.Person<
        id: ...,
        ...
      >
    ]
  >
]

В приведённом выше примере запроса мы предполагаем, что два «человека» вступили в отношения. Запрос показывает, как один человек добавляется со стороны :relationships, а другой — со стороны :reverse_relationships.

Также стоит отметить, что мы реализуем отдельные родительские модули для модулей Person и Relationship. Такое разделение обязанностей помогает улучшить организацию и сопровождаемость кода, позволяя изолировать основные функции для отношений в контексте MyApp.Relationships и наоборот.

Рассмотрим миграцию Ecto:

def change do
  create table(:relationships) do
    add :person_id, references(:people)
    add :relation_id, references(:people)
    timestamps()
  end

  create index(:relationships, [:person_id])
  create index(:relationships, [:relation_id])

  create unique_index(
    :relationships,
    [:person_id, :relation_id],
    name: :relationships_person_id_relation_id_index
  )
end

Для более быстрого доступа в будущем мы создаём индексы и для :person_id, и для :relation_id. Затем создаём уникальный индекс для :relationships, чтобы люди не могли иметь дублирующиеся отношения. Наконец, мы передаём имя в параметр :name, чтобы было проще определить ограничение уникальности при работе с нашим changeset.

# In MyApp.Relationships.Relationship
@attrs [:person_id, :relation_id]

def changeset(struct, params \\ %{}) do
  struct
  |> Ecto.Changeset.cast(params, @attrs)
  |> Ecto.Changeset.unique_constraint(
    [:person_id, :relation_id],
    name: :relationships_person_id_relation_id_index
  )
end

Из-за самоссылочного характера связи нам нужно привести к типам только :join_keys, чтобы Ecto правильно связал две записи в базе данных. В приложениях для production-среды, скорее всего, потребуется добавить дополнительные атрибуты и проверки. Именно здесь разделение модулей поможет нам поддерживать порядок и управлять растущей сложностью.

Итоги

В этом руководстве мы использовали ассоциации many_to_many для реализации самоссылочного симметричного отношения.

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

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

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

← Предыдущая страница Мультитенантность с внешними ключами
Следующая страница → Полиморфные ассоциации «многие ко многим»

Пакет Hex Предпросмотр Hex (текущий файл) Просмотреть llms.txt Скачать версию ePub

Создано с помощью ExDoc (v0.40.2) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/self-referencing-many-to-many.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API