Ecto
Исходный кодEcto состоит из 4 основных компонентов:
Ecto.Repo— репозитории являются оболочками над хранилищем данных. Через репозиторий можно создавать, обновлять, удалять и запрашивать существующие записи. Для связи с базой данных репозиторию необходимы адаптер и учетные данныеEcto.Schema— схемы используются для преобразования внешних данных в структуры Elixir. Часто они служат для сопоставления таблиц базы данных с данными Elixir, но у них есть и множество других вариантов использованияEcto.Query— запросы, записанные в синтаксисе Elixir, используются для получения информации из заданного репозитория. Запросы Ecto безопасны и компонуемыEcto.Changeset— наборы изменений позволяют отслеживать и проверять изменения до их применения к данным
Вкратце:
-
Ecto.Repo— где находятся данные -
Ecto.Schema— что представляют собой данные -
Ecto.Query— как читать данные -
Ecto.Changeset— как изменять данные
Помимо четырех перечисленных компонентов, большинство разработчиков используют Ecto для работы с базами данных SQL, такими как PostgreSQL и MySQL, через проект ecto_sql. ecto_sql предоставляет множество удобных средств для работы с базами данных SQL, а также позволяет версионировать изменения базы данных с течением времени с помощью миграций базы данных.
Чтобы быстро ознакомиться с примером приложения, использующего Ecto, изучите руководство по началу работы и сопутствующее демонстрационное приложение. В файле README Ecto также приведены ссылки на другие ресурсы.
В следующих разделах мы рассмотрим эти компоненты и то, как они взаимодействуют друг с другом. Для более конкретных примеров, параметров и настроек обратитесь к документации соответствующих модулей.
Репозитории
Ecto.Repo — это оболочка над базой данных. Репозиторий можно определить следующим образом:
defmodule Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
end
Конфигурация Repo должна находиться в окружении приложения, обычно она задается в файле config/config.exs:
config :my_app, Repo, database: "ecto_simple", username: "postgres", password: "postgres", hostname: "localhost", # OR use a URL to connect instead url: "postgres://postgres:postgres@localhost/ecto_simple"
Каждый репозиторий в Ecto определяет функцию start_link/0, которую необходимо вызвать перед использованием репозитория. Обычно эта функция не вызывается напрямую, а используется как часть дерева супервизии приложения.
Если приложение было сгенерировано с супервизором (для этого передайте --sup команде mix new), у вас будет файл lib/my_app/application.ex с функцией обратного вызова запуска приложения, которая определяет и запускает супервизор. Достаточно изменить функцию start/2, чтобы запустить репозиторий как супервизор в супервизоре приложения:
def start(_type, _args) do
children = [
MyApp.Repo,
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
Схема
Схемы позволяют разработчикам задавать структуру данных. Рассмотрим пример:
defmodule Weather do
use Ecto.Schema
# weather is the DB table
schema "weather" do
field :city, :string
field :temp_lo, :integer
field :temp_hi, :integer
field :prcp, :float, default: 0.0
end
end
При определении схемы Ecto автоматически создает структуру с полями схемы:
iex> weather = %Weather{temp_lo: 30}
iex> weather.temp_lo
30
Схема также позволяет взаимодействовать с репозиторием:
iex> weather = %Weather{temp_lo: 0, temp_hi: 23}
iex> Repo.insert!(weather)
%Weather{...}
После сохранения weather в базе данных будет возвращена его новая копия %Weather{} с установленным первичным ключом (id). Это значение можно использовать, чтобы получить структуру из репозитория:
# Get the struct back
iex> weather = Repo.get Weather, 1
%Weather{id: 1, ...}
# Delete it
iex> Repo.delete!(weather)
%Weather{...}
ПРИМЕЧАНИЕ: при использовании
Ecto.Schemaпо умолчанию создается поле:idтипа:id(:id означает :integer), которое является первичным ключом схемы. Если требуется другой первичный ключ, можно объявить пользовательские@primary_keyдо вызоваschema/2. Дополнительную информацию см. в документацииEcto.Schema.
Обратите внимание, что хранилище (репозиторий) и данные не зависят друг от друга. Это дает два основных преимущества:
Использование структур в качестве данных гарантирует, что они будут легковесными и сериализуемыми. Во многих языках данные часто представлены крупными сложными объектами со взаимосвязанными транзакциями состояния, что затрудняет сериализацию, сопровождение и понимание;
Для взаимодействия с репозиториями не нужно определять схемы: такие операции, как
all,insert_allи другие, позволяют разработчикам напрямую обращаться к данным и изменять их, сохраняя возможность при необходимости работать непосредственно с базой данных;
Наборы изменений
Хотя в приведенном выше примере мы напрямую добавляли структуру в репозиторий и удаляли ее оттуда, операции со схемами выполняются через наборы изменений, чтобы Ecto мог эффективно отслеживать изменения.
Наборы изменений позволяют разработчикам отбирать, приводить к нужным типам и проверять изменения до их применения к данным. Рассмотрим следующую схему:
defmodule User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name
field :email
field :age, :integer
end
def changeset(user, params \\ %{}) do
user
|> cast(params, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_inclusion(:age, 18..100)
end
end
Функция changeset/2 сначала вызывает Ecto.Changeset.cast/4, передавая ей структуру, параметры и список разрешенных полей; в результате возвращается набор изменений. Параметры представляют собой карту с двоичными ключами и значениями, которые приводятся к типам, заданным в схеме.
Любые параметры, явно не указанные в списке полей, будут проигнорированы.
После приведения типов набор изменений передается различным функциям Ecto.Changeset.validate_*, которые проверяют только измененные поля. Иными словами, если поле не было передано в качестве параметра, оно вообще не будет проверяться. Например, если карта params содержит только ключи "name" и "email", проверка "age" не будет запущена.
После создания набор изменений можно передать таким функциям репозитория, как insert и update, которые возвращают кортеж :ok или :error:
case Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
Преимущество явных наборов изменений состоит в том, что для разных сценариев использования можно легко предоставлять разные наборы. Например, можно определить отдельные наборы изменений для регистрации пользователей и обновления их данных:
def registration_changeset(user, params) do # Changeset on create end def update_changeset(user, params) do # Changeset on update end
Наборы изменений также умеют преобразовывать ограничения базы данных, например уникальные индексы и проверки внешних ключей, в ошибки. Это позволяет разработчикам поддерживать целостность базы данных и при этом предоставлять конечным пользователям понятную обратную связь. Примеры см. в разделе Ecto.Changeset.unique_constraint/3, а также в описании других функций _constraint.
Запросы
И наконец, Ecto позволяет писать запросы на Elixir и отправлять их в репозиторий, который преобразует их для базовой базы данных. Рассмотрим пример:
import Ecto.Query, only: [from: 2]
query = from u in User,
where: u.age > 18 or is_nil(u.email),
select: u
# Returns %User{} structs matching the query
Repo.all(query)
В приведенном выше примере мы использовали схему, но запросы можно также выполнять непосредственно к таблице, указав ее имя в виде строки. В этом случае необходимо явно указать извлекаемые данные:
query = from u in "users",
where: u.age > 18 or is_nil(u.email),
select: %{name: u.name, age: u.age}
# Returns maps as defined in select
Repo.all(query)
Запросы определяются и расширяются с помощью макроса from. Поддерживаются следующие ключевые слова:
:distinct:where:order_by:offset:limit:lock:group_by:having:join:select:preload
Примеры и подробная документация по каждому из них доступны в модуле Ecto.Query. Список функций, поддерживаемых в запросах, приведен в разделе Ecto.Query.API.
При написании запроса вы находитесь в синтаксисе запросов Ecto. Чтобы обращаться к значениям параметров или вызывать функции Elixir, необходимо использовать оператор ^, переопределенный в Ecto:
def min_age(min) do from u in User, where: u.age > ^min end
Помимо Repo.all/1, которая возвращает все записи, репозитории также предоставляют Repo.one/1, которая возвращает одну запись или nil, Repo.one!/1), которая возвращает одну запись или вызывает исключение, Repo.get/2, которая извлекает записи с указанным идентификатором, и другие функции.
Если требуется обойти ограничения, Ecto предоставляет фрагменты (см. Ecto.Query.API.fragment/1) для вставки в запросы фрагментов SQL (и не только SQL). Кроме того, большинство адаптеров предоставляют прямые API для запросов, например Ecto.Adapters.SQL.query/4, позволяя разработчикам полностью обходить запросы Ecto.
Другие темы
Связи
Ecto поддерживает определение связей в схемах:
defmodule Post do
use Ecto.Schema
schema "posts" do
has_many :comments, Comment
end
end
defmodule Comment do
use Ecto.Schema
schema "comments" do
field :title, :string
belongs_to :post, Post
end
end
При определении связи Ecto также создает в схеме поле с именем этой связи. По умолчанию данные связей в это поле не загружаются:
iex> post = Repo.get(Post, 42) iex> post.comments #Ecto.Association.NotLoaded<...>
Однако разработчики могут использовать предварительную загрузку в запросах, чтобы автоматически заполнить это поле:
Repo.all from p in Post, preload: [:comments]
Предварительную загрузку также можно выполнить с помощью предварительно определенного значения соединения:
Repo.all from p in Post,
join: c in assoc(p, :comments),
where: c.votes > p.votes,
preload: [comments: c]
Наконец, в простых случаях предварительную загрузку можно выполнить после получения коллекции:
posts = Repo.all(Post) |> Repo.preload(:comments)
Модуль Ecto также предоставляет удобные средства для работы со связями. Например, Ecto.assoc/3 возвращает запрос со всеми связанными данными для заданной структуры:
import Ecto # Get all comments for the given post Repo.all assoc(post, :comments) # Or build a query on top of the associated comments query = from c in assoc(post, :comments), where: not is_nil(c.title) Repo.all(query)
Другая функция модуля Ecto — build_assoc/3, которая позволяет создать связанную структуру с нужными полями:
Repo.transact(fn ->
post = Repo.insert!(%Post{title: "Hello", body: "world"})
# Build a comment from post
comment = Ecto.build_assoc(post, :comments, body: "Excellent!")
Repo.insert!(comment)
end)
В приведенном выше примере Ecto.build_assoc/3 эквивалентна следующему:
%Comment{post_id: post.id, body: "Excellent!"}
Дополнительную информацию об определении связей и соответствующих модулях можно найти в документации Ecto.Schema.
ПРИМЕЧАНИЕ: Ecto не загружает связи лениво. Хотя на первый взгляд такая загрузка может показаться удобной, со временем она становится источником путаницы и проблем с производительностью.
Встраиваемые данные
Ecto также поддерживает встраиваемые данные. При использовании связей родительские и дочерние записи хранятся в разных таблицах, а при встраивании дочерняя запись хранится вместе с родительской.
Базы данных, такие как MongoDB, поддерживают встраиваемые данные на нативном уровне. В базах данных, таких как PostgreSQL, для этой функциональности используются комбинации столбцов JSONB (embeds_one/3) и ARRAY.
Дополнительную информацию см. в разделах Ecto.Schema.embeds_one/3 и Ecto.Schema.embeds_many/3.
Задачи и генераторы Mix
Ecto предоставляет множество задач, помогающих в работе, а также генераторы кода. Чтобы увидеть все доступные задачи, в проекте, где Ecto указан как зависимость, выполните команду mix help.
Генераторы Ecto автоматически откроют созданные файлы, если в переменной окружения задано значение ECTO_EDITOR.
Разрешение Repo
Перед использованием таких задач, как ecto.create и ecto.migrate, необходимо указать ключ :ecto_repos в конфигурации приложения. Например:
config :my_app, :ecto_repos, [MyApp.Repo] config :my_app, MyApp.Repo, database: "ecto_simple", username: "postgres", password: "postgres", hostname: "localhost"
Краткое описание
- assoc(struct_or_structs, assocs, opts \\ [])
Создает запрос для связи в заданной структуре или структурах.
- assoc_loaded?(list)
Проверяет, загружена ли связь.
- build_assoc(struct, assoc, attributes \\ %{})
Создает структуру из заданного
assocвstruct.- embedded_dump(data, format)
Сериализует заданную структуру, определенную встроенной схемой.
- embedded_load(schema_or_types, data, format)
Загружает ранее сериализованные
dataв заданнуюformatв схему.- get_meta(struct, atom)
Получает метаданные заданной структуры.
- primary_key(struct)
Возвращает первичные ключи схемы в виде списка ключевых слов.
- primary_key!(struct)
Возвращает первичные ключи схемы в виде списка ключевых слов.
- put_meta(struct, opts)
Возвращает новую структуру с обновленными метаданными.
- reset_fields(struct, fields)
Сбрасывает поля структуры к значениям по умолчанию.
Функции
Функции
assoc(struct_or_structs, assocs, opts \\ [])Исходный код
Создает запрос для связи в заданной структуре или структурах.
Примеры
В приведенном ниже примере мы получаем все комментарии, связанные с заданной публикацией:
post = Repo.get Post, 1 Repo.all Ecto.assoc(post, :comments)
assoc/3 также может принимать список публикаций, если он не пуст:
posts = Repo.all from p in Post, where: is_nil(p.published_at) Repo.all Ecto.assoc(posts, :comments)
Эту функцию также можно использовать для динамической загрузки связей, передав ей список. Например, чтобы получить всех авторов всех комментариев к заданным публикациям, выполните следующее:
posts = Repo.all from p in Post, where: is_nil(p.published_at) Repo.all Ecto.assoc(posts, [:comments, :author])
Параметры
-
:prefix— префикс, из которого загружаются связи. По умолчанию в запросах используется тот же префикс, что и у первой структуры в заданной коллекции. Этот параметр позволяет изменить префикс.
assoc_loaded?(list)Исходный код
Проверяет, загружена ли связь.
Примеры
iex> post = Repo.get(Post, 1) iex> Ecto.assoc_loaded?(post.comments) false iex> post = post |> Repo.preload(:comments) iex> Ecto.assoc_loaded?(post.comments) true
build_assoc(struct, assoc, attributes \\ %{})Исходный код
Создает структуру из заданного assoc в struct.
Примеры
Если отношение является has_one или has_many и в родительской структуре задан первичный ключ, этот ключ будет автоматически установлен в созданной связанной структуре:
iex> post = Repo.get(Post, 13)
%Post{id: 13}
iex> build_assoc(post, :comments)
%Comment{id: nil, post_id: 13}
Обратите внимание, что для случаев belongs_to это не работает, так как ключ часто является первичным и обычно генерируется динамически:
iex> comment = Repo.get(Comment, 13)
%Comment{id: 13, post_id: 25}
iex> build_assoc(comment, :post)
%Post{id: nil}
Также можно передать атрибуты в виде карты или списка ключевых слов, чтобы задать поля структуры, кроме ключа связи.
iex> build_assoc(post, :comments, text: "cool")
%Comment{id: nil, post_id: 13, text: "cool"}
iex> build_assoc(post, :comments, %{text: "cool"})
%Comment{id: nil, post_id: 13, text: "cool"}
iex> build_assoc(post, :comments, post_id: 1)
%Comment{id: nil, post_id: 13}
Ожидается, что заданные атрибуты будут структурированными данными. Если требуется создать связь из внешних данных, например параметров запроса, после вызова build_assoc/3 можно использовать Ecto.Changeset.cast/3:
parent |> Ecto.build_assoc(:child) |> Ecto.Changeset.cast(params, [:field1, :field2])
embedded_dump(data, format)Исходный код
@spec embedded_dump(Ecto.Schema.t(), format :: atom()) :: map()
Сериализует заданную структуру, определенную встроенной схемой.
Преобразует заданную встроенную схему в карту для сериализации в указанном формате. Например:
iex> Ecto.embedded_dump(%Post{}, :json)
%{title: "hello"} embedded_load(schema_or_types, data, format)Исходный код
@spec embedded_load( module_or_map :: module() | map(), data :: map(), format :: atom() ) :: Ecto.Schema.t() | map()
Загружает ранее сериализованные data в заданную format в схему.
Первый аргумент может быть модулем встроенной схемы или картой (типов) и определяет тип возвращаемого значения: соответственно структуру или карту.
Второй аргумент data задает загружаемые поля и значения. Это может быть карта, список ключевых слов или кортеж {fields, values}. Поля могут быть атомами или строками.
Третий аргумент format задает формат сериализованных данных. Например, базы данных могут сериализовать встроенные данные в :json; эта функция позволяет загружать такие данные обратно в схемы. При использовании пользовательских типов Ecto вызовет обратный вызов Ecto.Type.embed_as/1, чтобы определить, следует ли загружать данные с помощью cast или load.
Поля, отсутствующие в схеме (или карте types), игнорируются. Если тип какого-либо значения недопустим, возникает ошибка.
Если нужно загрузить данные в невстроенную схему, которая была напрямую сохранена в заданном репозитории, используйте Ecto.Repo.load/2.
Примеры
iex> result = Ecto.Adapters.SQL.query!(MyRepo, "SELECT users.settings FROM users", [])
iex> Enum.map(result.rows, fn [settings] -> Ecto.embedded_load(Setting, Jason.decode!(settings), :json) end)
[%Setting{...}, ...] get_meta(struct, atom)Исходный код
Получает метаданные заданной структуры.
Например, чтобы проверить, была ли она сохранена:
iex> Ecto.get_meta(changeset.data, :state) :built
См. Ecto.Schema.Metadata.
primary_key(struct)Исходный код
@spec primary_key(Ecto.Schema.t()) :: Keyword.t()
Возвращает первичные ключи схемы в виде списка ключевых слов.
primary_key!(struct)Исходный код
@spec primary_key!(Ecto.Schema.t()) :: Keyword.t()
Возвращает первичные ключи схемы в виде списка ключевых слов.
Вызывает исключение Ecto.NoPrimaryKeyFieldError, если в схеме нет поля первичного ключа.
put_meta(struct, opts)Исходный код
@spec put_meta(Ecto.Schema.schema(), meta) :: Ecto.Schema.schema()
when meta: [
source: Ecto.Schema.source(),
prefix: Ecto.Schema.prefix(),
context: Ecto.Schema.Metadata.context(),
state: Ecto.Schema.Metadata.state()
] Возвращает новую структуру с обновленными метаданными.
Можно задать следующие параметры:
-
:source— изменяет источник запроса структуры -
:prefix— изменяет префикс запроса структуры -
:context— изменяет контекст метаданных структуры -
:state— изменяет состояние структуры
См. Ecto.Schema.Metadata.
reset_fields(struct, fields)Исходный код
@spec reset_fields(Ecto.Schema.t(), list()) :: Ecto.Schema.t()
Сбрасывает поля структуры к значениям по умолчанию.
Примеры
iex> post = post |> Repo.preload(:author)
%Post{title: "hello world", author: %Author{}}
iex> Ecto.reset_fields(post, [:title, :author])
%Post{
title: "default title",
author: #Ecto.Association.NotLoaded<association :author is not loaded>
}
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.html