Elixir — это современный, динамичный, функциональный язык программирования, используемый для создания высокораспределенных и отказоустойчивых приложений. Ecto является основной библиотекой для работы с базами данных, предоставляя нам инструменты для взаимодействия с базами данных в рамках общего API, создания версий базы данных вместе с нашим приложением и обработки данных в нашем приложении.
В этой статье кратко рассматриваются основные аспекты библиотеки Ecto. Хотя он написан для Ecto 1.x, примеры в этой статье прямо совместимы с Ecto 2, и там, где есть несоответствия, они упоминаются. Базовые знания об эликсире и миксе предполагаются.
Приложение
Мы создадим очень простое приложение с нуля, которое будет хранить и получать заметки для нас. Сделав это, мы рассмотрим каждый из четырех основных компонентов Ecto: репозитории, схемы, наборы изменений и запросы.
Создание нового приложения
Давайте начнем с создания нового приложения Mix:
mix new notex --sup
Флаг --sup
генерирует дополнительный шаблонный код, который требуется для приложения OTP. Это приложение должно иметь дерево контроля, потому что оно необходимо Ecto (подробнее об этом через минуту).
Настройка зависимостей
Теперь давайте обновим наш файл mix.exs
с некоторыми зависимостями приложения. Для этого мы хотим указать Ecto и один из его адаптеров. Я решил использовать MySQL для этого, поэтому нам нужно указать библиотеку Mariaex (Ecto поддерживает несколько баз данных ).
Обновите функцию application/0
в файле mix.exs
следующим образом:
def application do [applications: [:logger, :ecto, :mariaex], mod: {Notex, []}] end
И обновите deps/0
следующим:
defp deps do [{:ecto, "~> 1.1.5"}, # or "~> 2.0" for Ecto 2 {:mariaex, "~> 0.6.0"}] # or "~> 0.7.0" for Ecto 2 end
Теперь загрузите зависимости с помощью mix deps.get
.
Далее нам нужно интегрировать эти зависимости в наше приложение. Это будет включать создание нового модуля-оболочки для хранилища Ecto, обновление дерева контроля нашего приложения для запуска и контроля этого нового модуля, а также настройку информации о подключении адаптера.
Давайте сначала начнем с определения модуля Notex.Repo
в lib/notex/repo.ex
со следующим кодом:
defmodule Notex.Repo do use Ecto.Repo, otp_app: :notex end
Расположение этого модуля ( lib/app_name/repo.ex
) является стандартным. Каждый раз, когда мы используем команду mix ecto
, она по умолчанию ищет определенный репозиторий на AppName.Repo
. Мы можем разместить его в другом месте, но это будет --repo
с неудобством необходимости указывать его местоположение с помощью флага -r
(или --repo
).
Вышеупомянутый модуль Notex.Repo
позволяет нам работать с базами данных, используя Ecto. Он делает это, во-первых, вводя функции из модуля Repo
Ecto (которые предоставляют API запросов к базе данных), и во-вторых, называя наше приложение OTP как :notex
.
Репозиторий Ecto предоставляет нам общий интерфейс для взаимодействия с базовой базой данных (что определяется используемым адаптером). Таким образом, хотя Ecto использует терминологический репозиторий, он не следует шаблону проектирования репозитория, поскольку является оболочкой для базы данных, а не для таблицы.
Теперь, когда мы определили модуль Notex.Repo
, мы должны теперь добавить это к нашему дереву контроля в модуле Notex
(в lib/notex.ex
). Обновите функцию start/2
следующим образом:
def start(_type, _args) do import Supervisor.Spec, warn: false children = [ supervisor(Notex.Repo, []), ] opts = [strategy: :one_for_one, name: Notex.Supervisor] Supervisor.start_link(children, opts) end
Мы добавили модуль Notex.Repo
в качестве дочернего супервизора (поскольку он сам является контролирующим OTP-приложением). Это означает, что оно будет контролироваться нашим приложением OTP, и наше приложение будет отвечать за его запуск при запуске приложения.
Каждое соединение, созданное с помощью Ecto, использует отдельный процесс (где процесс извлекается из пула процессов с использованием библиотеки под названием Poolboy ). Это сделано для того, чтобы наши запросы могли выполняться одновременно, а также быть устойчивыми к сбоям (например, тайм-аутам). Поэтому нашему приложению требуется OTP, потому что Ecto имеет свои собственные процессы, которые требуют контроля (включая дерево контроля, контролирующее пул соединений с базой данных). Это можно увидеть с помощью библиотеки Erlang Observer , которая позволяет нам визуализировать процессы в приложении.
После добавления репо к нашим рабочим процессам, подлежащим контролю, нам необходимо наконец настроить адаптер, чтобы он мог взаимодействовать с нашей базой данных. Поместите следующий код в конец файла `config / config.exs` (при необходимости обновите данные):
config :notex, Notex.Repo, adapter: Ecto.Adapters.MySQL, database: "notex", username: "root", password: "root", hostname: "localhost" # uncomment the following line if Ecto 2 is being used # config :notex, ecto_repos: [Notex.Repo]
Здесь мы указываем имя нашего OTP-приложения ( :notex
) и имя нашего только что определенного модуля ( Notex.Repo
) для обеспечения связи с базой данных. Другие параметры конфигурации должны быть понятны. Ecto 2 требует, чтобы мы дополнительно указали список репозиториев Ecto, которые мы используем в нашем приложении.
На самом деле Ecto предоставляет нам ярлык для настройки вышеуказанного модуля Repo
в качестве смешанной задачи: mix ecto.gen.repo
. Это создает модуль репозитория для нас и обновляет файл config.exs
с некоторой базовой конфигурацией (хотя модуль Repo
все еще необходимо вручную добавить в дерево контроля). Я избегал использовать его здесь в основном из-за дидактических причин, показывающих, как настроить Ecto вручную (и из-за того, что генератор репо предполагает, что вы используете Postgres, поэтому в любом случае нам пришлось бы обновить адаптер в конфигурации).
Прежде чем двигаться дальше, давайте очень кратко рассмотрим иерархию процессов. (Обратите внимание, что если вы работаете с Ecto 2, вам сначала нужно создать базу данных со mix ecto.create
прежде чем пытаться скомпилировать проект.) Запустите наше приложение в интерактивной оболочке Elixir, а затем запустите наблюдатель:
iex -S mix iex(1)> :observer.start :ok
Перейдя на вкладку « Приложение », мы можем увидеть процессы приложения, в том числе те, которые являются супервизорами:
Вот почему это приложение должно быть приложением OTP. Но это так же далеко вниз по кроличьей норе, о которой мы поговорим в отношении процессов и OTP в этой статье. Они будут рассмотрены более подробно в следующих статьях.
Создание базы данных и таблиц
Теперь с этой настройкой мы можем создать нашу базу данных и таблицы. Чтобы создать базу данных, выполните следующую команду:
mix ecto.create
Для создания таблиц мы будем использовать функцию миграции Ecto. Миграции позволяют нам создавать версии базы данных вместе с исходным кодом, что позволяет отслеживать изменения и применять различные состояния. Поэтому мы создаем новые миграции всякий раз, когда хотим изменить структуру базы данных.
Новая миграция может быть создана с помощью команды mix ecto.gen.migration
следующим образом:
mix ecto.gen.migration create_notes_table
Выше следует создать новую папку миграции в priv/repo/migrations
, а также новый файл миграции. К этому файлу добавляется дата и время создания (для упрощения упорядочивания каталогов), а также наше имя миграции. Откройте этот файл и измените его следующим образом:
defmodule Notex.Repo.Migrations.CreateNotesTable do use Ecto.Migration def change do create table(:notes) do add :note_name, :string add :note_content, :string end end end
Для простоты мы использовали макрос create
чтобы определить новую таблицу (называемую notes
) с двумя полями: note_name
и note_content
. Первичный ключ для нас создается автоматически (с именем id
). Хотя оба наших поля были определены как простые строки, Ecto поддерживает множество типов, которые вы можете проверить в его документации .
Теперь, когда наша миграция завершена, мы можем запустить миграцию с помощью следующей команды:
mix ecto.migrate
Это создаст нашу таблицу notes
с 3 полями (третье поле — id
, первичный ключ).
После создания таблицы пришло время создать модель для таблицы. Модель используется для определения полей таблицы и их соответствующих типов. Они будут использоваться приложением и запрашивающим DSL Ecto при приведении и проверке данных. Определения модели могут также содержать виртуальные поля (в отличие от определений миграции), которые используются для хранения, как правило, эфемерных данных, которые мы не хотим сохранять (таких как нехэшированные пароли).
В своей самой простой форме наша модель Notex.Note
(расположенная в lib/notex/note.ex
) будет выглядеть следующим образом:
defmodule Notex.Note do use Ecto.Schema schema "notes" do field :note_name, :string field :note_content, :string end end
Мы Ecto.Schema
модуль Ecto.Schema
чтобы мы могли использовать макрос schema
для определения полей и их типов. Эти определения станут важными позже, когда мы использовали наборы изменений Ecto. Что-то еще, что макрос schema
делает для нас, это определение структуры типа в качестве текущего модуля (в данном случае это %Notex.Note{}
). Эта структура позволит нам создавать новые наборы изменений (подробнее об этом в ближайшее время) и вставлять данные в таблицу.
С помощью всего вышеперечисленного мы можем запустить IEx и начать запрашивать нашу базу данных:
iex(1)> import Ecto.Query nil iex(2)> Notex.Repo.all(from n in Notex.Note, select: n.note_name) []
(Отладочная информация консоли отредактирована.)
Модуль Ecto Query
импортируется, чтобы сделать все запрашивающие макросы DSL (например, from
) доступными для нас в оболочке. Затем мы создаем простой запрос для возврата всех записей (используя all/1
), выбирая только поле note_name
. Это возвращает пустой список, так как в настоящее время у нас нет записей в базе данных. Давайте создадим новый набор изменений и вставим его в таблицу:
iex(1)> import Ecto.Query nil iex(2)> changeset = Ecto.Changeset.change(%Notex.Note{note_name: "To Do List", note_content: "Finish this article"}) %Ecto.Changeset{action: nil, changes: %{}, constraints: [], errors: [], filters: %{}, model: %Notex.Note{__meta__: #Ecto.Schema.Metadata<:built>, id: nil, note_content: "Finish this article", note_name: "To Do List"}, optional: [], opts: [], params: nil, prepare: [], repo: nil, required: [], types: %{id: :id, note_content: :string, note_name: :string}, valid?: true, validations: []} iex(3)> Notex.Repo.insert(changeset) {:ok, %Notex.Note{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 2, note_content: "Finish this article", note_name: "To Do List"}} iex(4)> Notex.Repo.all(from n in Notex.Note, select: n.note_name) ["To Do List"]
(Отладочная информация консоли отредактирована.)
Мы начнем с импорта Ecto.Query
снова, который необходим для последней операции выборки (особенно для макроса from
). Затем мы используем функцию change/1
из Ecto.Changeset
чтобы создать новый набор изменений, используя структуру %Notex.Note{}
. Этот набор изменений затем вставляется, а затем извлекается.
Наборы изменений — это то, что мы используем при работе с записями. Они позволяют нам отслеживать изменения данных до их вставки, а также проверять эти изменения и приводить их значения к правильным типам данных (согласно определению нашей схемы). Как видно из вышесказанного, структура %Ecto.Changeset{}
содержит ряд членов, которые будут полезны для %Ecto.Changeset{}
допустимости changeset.valid?
( changeset.valid?
), Каковы ошибки, если их не было ( changeset.errors
) и так далее.
Давайте обновим модель Notex.Note
чтобы продемонстрировать некоторые Notex.Note
изменений и операции запросов, поскольку выполнение их в IEx становится немного беспорядочным:
defmodule Notex.Note do use Ecto.Schema import Ecto.Changeset, only: [cast: 4] import Ecto.Query, only: [from: 2] alias Notex.Note alias Notex.Repo schema "notes" do field :note_name, :string field :note_content, :string end @required_fields ~w(note_name) @optional_fields ~w(note_content) def insert_note(%{} = note) do %Note{} |> cast(note, @required_fields, @optional_fields) |> Repo.insert! end def get_notes do query = from n in Note, select: {n.id, n.note_name} query |> Repo.all end def get_note(note_id) do Repo.get!(Note, note_id) end def update_note(%{"id" => note_id} = note_changes) do Repo.get!(Note, note_id) |> cast(note_changes, @required_fields, @optional_fields) |> Repo.update! end def delete_note(note_id) do Repo.get!(Note, note_id) |> Repo.delete! end end
Давайте рассмотрим каждую из пяти новых функций. Функция insert_note/1
создает новую заметку для нас. Функция cast/4
обрабатывает приведение данных из полей ввода к соответствующим им типам полей (согласно нашему определению схемы), а также гарантирует, что все обязательные поля имеют значения. Набор изменений, возвращенный из cast / 4, затем вставляется в базу данных. Обратите внимание, что в Ecto 2 функции cast/3
и validate_required/3
должны использоваться вместо cast/4
.
Функция get_notes/0
возвращает список кортежей всех заметок в таблице. Это делается путем сопоставления с образцом в операторе выбора. (Вместо этого мы могли бы легко вернуть список карт, например, с помощью select: %{id: n.id, note_name: n.note_name}
.)
Функция get_note/1
извлекает одну заметку из таблицы в соответствии с идентификатором заметки. Это делается через get!
функция, которая либо возвращает заметку при успехе, либо выбрасывает при неудаче.
Функция update_note/1
обновляет заметку в соответствии с предоставленным идентификатором заметки. Обратите внимание на строковый ключ на карте сигнатуры функции (ключ id
). Это соглашение, которое я взял из среды Phoenix, где неанизированные данные (обычно предоставляемые пользователем) представлены в картах со строковыми ключами, а очищенные данные представлены в картах с атомными ключами. Чтобы выполнить обновление, мы сначала извлекаем заметку в соответствии с ее идентификатором из базы данных, затем используем функцию cast/4
чтобы применить изменения к записи, прежде чем окончательно вставить обновленный набор изменений обратно в базу данных.
Функция delete_note/1
удаляет заметку из базы данных. Сначала мы извлекаем заметку из базы данных через ее идентификатор (аналогично функции update_note/1
), а затем удаляем ее, используя возвращенную структуру Note
.
Имея указанные выше операции CRUD, давайте вернемся к IEx и попробуем:
iex(1)> alias Notex.Note nil iex(2)> Note.insert_note(%{"note_name" => "To Do's", "note_content" => "Finish this article..."}) {:ok, %Notex.Note{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 6, note_content: "Finish this article...", note_name: "To Do's"}} iex(3)> Note.get_notes [{6, "To Do's"}] iex(4)> Note.get_note(6) %Notex.Note{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 6, note_content: "Finish this article...", note_name: "To Do's"} iex(5)> Note.update_note(%{"id" => 6, "note_name" => "My To Do List"}) {:ok, %Notex.Note{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 6, note_content: "Finish this article...", note_name: "My To Do List"}} iex(6)> Note.get_note(6) %Notex.Note{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 6, note_content: "Finish this article...", note_name: "My To Do List"} iex(7)> Note.delete_note(6) {:ok, %Notex.Note{__meta__: #Ecto.Schema.Metadata<:deleted>, id: 6, note_content: nil, note_name: nil}} iex(8)> Note.get_notes []
(Отладочная информация консоли отредактирована.)
И вот оно, базовое CRUD-приложение, использующее Ecto! Мы могли бы сделать вывод и сделать API более привлекательным для запросов, но я оставлю это как расширение, поскольку они касаются того, что мы охватываем (и эта статья, я думаю, достаточно длинна).
Вывод
В этой статье были рассмотрены основы Ecto путем создания простого приложения CRUD с нуля. Мы увидели множество возможностей пакетов Ecto для управления записями и изменениями базы данных, включая миграции, схемы и запросы DSL, а также касались таких тем, как OTP. Я надеюсь, что это послужило хорошим руководством для тех, кто хочет освоить работу с базами данных в Elixir!
В следующей статье я расскажу об основах Elixir Ecto Querying DSL .