Spec-Zone.ru › Phoenix

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>
}

Пакет Hex Предпросмотр Hex Просмотреть llms.txt Скачать версию ePub

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

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.html

Spec-Zone.ru

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