Самоссылочная связь «многие ко многим»
Исходный код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 и упростить её сопровождение.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/self-referencing-many-to-many.html