Ecto.Type поведение
Исходный кодОпределяет функции и поведение Ecto.Type для реализации базовых пользовательских типов.
Ecto предоставляет два типа пользовательских типов: базовые и параметризованные. Базовые типы просты: для их реализации достаточно четырёх обратных вызовов, чего хватает в большинстве случаев. Параметризованные типы можно настраивать при определении поля, и они предоставляют широкий набор обратных вызовов.
Определение базовых пользовательских типов и все их обратные вызовы доступны в этом модуле. Подробнее о параметризованных типах можно узнать в Ecto.ParameterizedType. Если сомневаетесь, отдавайте предпочтение базовым пользовательским типам и используйте параметризованные, только если вам нужны дополнительные возможности.
Внешнее, внутреннее и представленное в базе данных значения
Основная функция пользовательского типа — сопоставление внешнего, внутреннего и представленного в базе данных значений, принадлежащих этому типу.
Определения внешних и внутренних данных см. в соответствующем разделе документации по changeset.
stateDiagram-v2 external: External Data internal: Internal Data database: Database Data external --> internal: cast/1 internal --> database: dump/1 database --> internal: load/1
Пример
Предположим, вы хотите хранить структуру URI в схеме сервиса сокращения URL-адресов. Во время выполнения в Ecto нет типа поля для такого значения, поэтому нужен пользовательский тип.
Кроме того, вы хотите выполнять запросы не только по полному URL-адресу, но, например, и по конкретным используемым портам. Это можно сделать, сохранив данные URI в поле map, а не просто в виде строки.
from s in ShortUrl,
where: fragment("?->>? ILIKE ?", s.original_url, "port", "443")
Таким образом, пользовательский тип должен обрабатывать преобразование внешних данных в данные времени выполнения (cast/1), а также преобразование этих данных времени выполнения во встроенный тип Ecto :map и обратно (dump/1 и load/1).
defmodule EctoURI do
use Ecto.Type
def type, do: :map
# Provide custom casting rules.
# Cast strings into the URI struct to be used at runtime
def cast(uri) when is_binary(uri) do
{:ok, URI.parse(uri)}
end
# Accept casting of URI structs as well
def cast(%URI{} = uri), do: {:ok, uri}
# Everything else is a failure though
def cast(_), do: :error
# When loading data from the database, as long as it's a map,
# we just put the data back into a URI struct to be stored in
# the loaded schema struct.
def load(data) when is_map(data) do
data =
for {key, val} <- data do
{String.to_existing_atom(key), val}
end
{:ok, struct!(URI, data)}
end
# When dumping data to the database, we *expect* a URI struct
# but any value could be inserted into the schema struct at runtime,
# so we need to guard against them.
def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
def dump(_), do: :error
end
Теперь мы можем использовать созданный тип поля в наших схемах:
defmodule ShortUrl do
use Ecto.Schema
schema "posts" do
field :original_url, EctoURI
end
end
Примечание: значения nil всегда обходят пользовательские типы и не могут ими обрабатываться.
use Ecto.Type
При вызове use Ecto.Type будет задано @behaviour Ecto.Type и определены реализации по умолчанию, которые можно переопределить, для embed_as/1 и equal?/2. Если при экспорте из Ecto вы хотите, чтобы вызывался ваш dump/1, необходимо реализовать собственную функцию embed_as/1.
Пользовательские типы и первичные ключи
Помните: если вы измените тип первичных ключей, необходимо также изменить тип всех ассоциаций, ссылающихся на эти первичные ключи.
Предположим, вы хотите закодировать идентификаторы, чтобы пользователи не могли перебирать содержимое вашего приложения. Тип Ecto может преобразовывать закодированную версию идентификатора в его представление в базе данных и обратно. Для простоты в этом примере воспользуемся кодированием base64:
defmodule EncodedId do
use Ecto.Type
def type, do: :id
def cast(id) when is_integer(id) do
{:ok, encode_id(id)}
end
def cast(_), do: :error
def dump(id) when is_binary(id) do
{:ok, id_decoded} = Base.decode64(id)
{:ok, String.to_integer(id_decoded)}
end
def load(id) when is_integer(id) do
{:ok, encode_id(id)}
end
defp encode_id(id) do
id
|> Integer.to_string()
|> Base.encode64()
end
end
Чтобы использовать его как тип идентификатора в нашей схеме, можно воспользоваться атрибутом модуля @primary_key:
defmodule BlogPost do
use Ecto.Schema
@primary_key {:id, EncodedId, autogenerate: true}
schema "posts" do
belongs_to :author, Author, type: EncodedId
field :content, :string
end
end
defmodule Author do
use Ecto.Schema
@primary_key {:id, EncodedId, autogenerate: true}
schema "authors" do
field :name, :string
has_many :posts, BlogPost
end
end
Атрибут @primary_key сообщит ecto, какой тип использовать для идентификатора.
Обратите внимание на параметр type: EncodedId, переданный в belongs_to в схеме BlogPost. По умолчанию Ecto рассматривает ассоциации так, будто их ключи имеют тип :integer. Наши первичные ключи имеют пользовательский тип, поэтому Ecto завершит работу с ошибкой при попытке привести эти идентификаторы к нужному типу.
В качестве альтернативы после @primary_key можно задать @foreign_key_type EncodedId, чтобы автоматически настроить тип всех полей belongs_to.
Сводка
- custom()
Пользовательские типы представлены модулями, определёнными пользователем.
- primitive()
Примитивные типы Ecto (обрабатываются Ecto).
- t()
Тип Ecto: примитивный или пользовательский.
Типы
- autogenerate()
Создаёт загруженную версию данных.
- cast(term)
Приводит переданное значение к пользовательскому типу.
- dump(term)
Преобразует переданное значение во встроенный тип Ecto.
- embed_as(format)
Определяет, как следует обрабатывать тип внутри встроенных структур.
- equal?(term, term)
Проверяет, равны ли два значения семантически.
- load(term)
Загружает переданное значение в пользовательский тип.
- type()
Возвращает базовый тип схемы для пользовательского типа.
Обратные вызовы
- base?(atom)
Проверяет, можно ли использовать переданный атом как базовый тип.
- cast(type, value)
Приводит значение к указанному типу.
- cast!(type, value)
Приводит значение к указанному типу или вызывает ошибку.
- composite?(atom)
Проверяет, можно ли использовать переданный атом как составной тип.
- dump(type, value, dumper \\ &dump/2)
Преобразует значение в указанный тип.
- embed_as(base, format)
Возвращает способ обработки типа внутри встроенных структур для указанного формата.
- embedded_dump(type, value, format)
Преобразует
valueдляtypeс учётом того, что он будет встроен вformat.- embedded_load(type, value, format)
Загружает
valueдляtypeс учётом того, что он был встроен вformat.- equal?(type, term1, term2)
Проверяет, равны ли два значения.
- format(type)
Форматирует тип для сообщений об ошибках и журналов.
- include?(type, term, collection)
Проверяет, содержит ли
collectionзначениеterm.- load(type, value, loader \\ &load/2)
Загружает значение с указанным типом.
- match?(schema_type, query_type)
Проверяет, соответствует ли заданный тип примитивному типу, который может использоваться в запросах.
- parameterized?(arg1, module)
Проверяет, параметризован ли заданный тип указанным модулем.
- primitive?(base)
Проверяет, является ли тип примитивным.
- trim(type, value)
Обрезает значение в соответствии с его типом.
- type(type)
Возвращает базовый тип схемы для заданного, возможно пользовательского, типа.
Функции
Типы
base()Исходный код
@type base() :: :integer | :float | :boolean | :string | :bitstring | :map | :binary | :decimal | :id | :binary_id | :utc_datetime | :naive_datetime | :date | :time | :any | :utc_datetime_usec | :naive_datetime_usec | :time_usec | :duration
composite()Исходный код
@type composite() :: {:array, t()} | {:map, t()} | private_composite() custom()Исходный код
@type custom() :: module() | {:parameterized, {module(), term()}} Пользовательские типы представлены модулями, определёнными пользователем.
primitive()Исходный код
@type primitive() :: base() | composite()
Примитивные типы Ecto (обрабатываются Ecto).
t()Исходный код
@type t() :: primitive() | custom()
Тип Ecto: примитивный или пользовательский.
Обратные вызовы
autogenerate()Исходный код
@callback autogenerate() :: term()
Создаёт загруженную версию данных.
Этот обратный вызов вызывается, когда пользовательский тип передаётся в field с флагом :autogenerate.
cast(term)Исходный код
@callback cast(term()) :: {:ok, term()} | :error | {:error, keyword()} Приводит переданное значение к пользовательскому типу.
Этот обратный вызов вызывается для внешних данных и может возвращать значение любого типа, если функция dump/1 способна преобразовать возвращённое значение во встроенный тип Ecto. Этот обратный вызов вызывается в двух случаях:
- При приведении значений с помощью
Ecto.Changeset - При передаче аргументов в
Ecto.Query
Если переданное значение невозможно привести к нужному типу, можно вернуть :error. В changeset будет добавлено стандартное сообщение об ошибке «недопустимое значение».
Также можно вернуть {:error, keyword()}, чтобы настроить сообщение об ошибке changeset и его метаданные. Если передать ключ :message, он заменит стандартное сообщение. Переопределить ключ :type невозможно.
Для {:array, CustomType} или {:map, CustomType} возвращённый список ключевых слов будет удалён, а отображено стандартное сообщение об ошибке.
dump(term)Исходный код
@callback dump(term()) :: {:ok, term()} | :error Преобразует переданное значение во встроенный тип Ecto.
Этот обратный вызов получает любое значение, сохранённое в структуре. Он должен проверить его и преобразовать во встроенный тип Ecto.
embed_as(format)Исходный код
@callback embed_as(format :: atom()) :: :self | :dump
Определяет, как следует обрабатывать тип внутри встроенных структур.
По умолчанию тип передаётся без изменений, без вызова преобразования, чтобы сохранить представление более высокого уровня. Однако можно задать :dump, чтобы преобразовать его перед кодированием.
equal?(term, term)Исходный код
@callback equal?(term(), term()) :: boolean()
Проверяет, равны ли два значения семантически.
Этот обратный вызов используется для определения равенства типов в Ecto.Changeset.
По умолчанию значения сравниваются оператором равенства ==/2.
load(term)Исходный код
@callback load(term()) :: {:ok, term()} | :error Загружает переданное значение в пользовательский тип.
Этот обратный вызов вызывается при загрузке данных из базы данных и получает встроенный тип Ecto. Он может возвращать значение любого типа, если функция dump/1 способна преобразовать возвращённое значение обратно во встроенный тип Ecto.
type()Исходный код
@callback type() :: t()
Возвращает базовый тип схемы для пользовательского типа.
Например, если вы хотите предоставить собственные структуры для дат, функция type должна возвращать :date.
Обратите внимание: эта функция не обязана возвращать примитивные типы Ecto — достаточно, чтобы тип был известен адаптеру.
Функции
base?(atom)Исходный код
@spec base?(atom()) :: boolean()
Проверяет, можно ли использовать переданный атом как базовый тип.
iex> base?(:string) true iex> base?(:array) false iex> base?(Custom) false
cast(type, value)Исходный код
@spec cast(t(), term()) :: {:ok, term()} | {:error, keyword()} | :error Приводит значение к указанному типу.
cast/2 используется запросами поиска и changeset для приведения внешних значений к определённым типам.
Обратите внимание: nil можно привести ко всем примитивным типам, поскольку хранилища данных позволяют устанавливать nil в любом столбце.
Десятичные числа NaN и бесконечность не поддерживаются; вместо них используйте пользовательские типы.
iex> cast(:any, "whatever")
{:ok, "whatever"}
iex> cast(:any, nil)
{:ok, nil}
iex> cast(:string, nil)
{:ok, nil}
iex> cast(:integer, 1)
{:ok, 1}
iex> cast(:integer, "1")
{:ok, 1}
iex> cast(:integer, "1.0")
:error
iex> cast(:id, 1)
{:ok, 1}
iex> cast(:id, "1")
{:ok, 1}
iex> cast(:id, "1.0")
:error
iex> cast(:float, 1.0)
{:ok, 1.0}
iex> cast(:float, 1)
{:ok, 1.0}
iex> cast(:float, "1")
{:ok, 1.0}
iex> cast(:float, "1.0")
{:ok, 1.0}
iex> cast(:float, "1-foo")
:error
iex> cast(:boolean, true)
{:ok, true}
iex> cast(:boolean, false)
{:ok, false}
iex> cast(:boolean, "1")
{:ok, true}
iex> cast(:boolean, "0")
{:ok, false}
iex> cast(:boolean, "whatever")
:error
iex> cast(:string, "beef")
{:ok, "beef"}
iex> cast(:binary, "beef")
{:ok, "beef"}
iex> cast(:decimal, Decimal.new("1.0"))
{:ok, Decimal.new("1.0")}
iex> cast(:decimal, "1.0bad")
:error
iex> cast({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> cast({:array, :integer}, ["1", "2", "3"])
{:ok, [1, 2, 3]}
iex> cast({:array, :string}, [1, 2, 3])
:error
iex> cast(:string, [1, 2, 3])
:error
iex> cast(:utc_datetime, "2014-04-17T14:00:00Z")
{:ok, ~U[2014-04-17 14:00:00Z]}
iex> cast(:utc_datetime, "2014-04-17T14:00:00.030Z")
{:ok, ~U[2014-04-17 14:00:00Z]}
iex> cast(:utc_datetime, "2014-04-17T12:00:00-02:00")
{:ok, ~U[2014-04-17 14:00:00Z]} cast!(type, value)Исходный код
Приводит значение к указанному типу или вызывает ошибку.
Дополнительные сведения см. в cast/2.
Примеры
iex> Ecto.Type.cast!(:integer, "1") 1 iex> Ecto.Type.cast!(:integer, 1) 1 iex> Ecto.Type.cast!(:integer, nil) nil iex> Ecto.Type.cast!(:integer, 1.0) ** (Ecto.CastError) cannot cast 1.0 to :integer
composite?(atom)Исходный код
@spec composite?(atom()) :: boolean()
Проверяет, можно ли использовать переданный атом как составной тип.
iex> composite?(:array) true iex> composite?(:string) false
dump(type, value, dumper \\ &dump/2)Исходный код
@spec dump(t(), term(), (t(), term() -> {:ok, term()} | :error)) ::
{:ok, term()} | :error Преобразует значение в указанный тип.
В отличие от приведения, преобразование требует, чтобы возвращаемое значение было допустимым типом Ecto, поскольку оно будет передано в базовое хранилище данных.
iex> dump(:string, nil)
{:ok, nil}
iex> dump(:string, "foo")
{:ok, "foo"}
iex> dump(:integer, 1)
{:ok, 1}
iex> dump(:integer, "10")
:error
iex> dump(:binary, "foo")
{:ok, "foo"}
iex> dump(:binary, 1)
:error
iex> dump({:array, :integer}, [1, 2, 3])
{:ok, [1, 2, 3]}
iex> dump({:array, :integer}, [1, "2", 3])
:error
iex> dump({:array, :binary}, ["1", "2", "3"])
{:ok, ["1", "2", "3"]} embed_as(base, format)Исходный код
Возвращает способ обработки типа внутри встроенных структур для указанного формата.
См. embed_as/1.
embedded_dump(type, value, format)Исходный код
Преобразует value для type с учётом того, что он будет встроен в format.
Примеры
iex> Ecto.Type.embedded_dump(:decimal, Decimal.new("1"), :json)
{:ok, Decimal.new("1")} embedded_load(type, value, format)Исходный код
Загружает value для type с учётом того, что он был встроен в format.
Примеры
iex> Ecto.Type.embedded_load(:decimal, "1", :json)
{:ok, Decimal.new("1")} equal?(type, term1, term2)Исходный код
@spec equal?(t(), term(), term()) :: boolean()
Проверяет, равны ли два значения.
В зависимости от указанного type выполняется структурное или семантическое сравнение.
Примеры
iex> equal?(:integer, 1, 1)
true
iex> equal?(:decimal, Decimal.new("1"), Decimal.new("1.00"))
true format(type)Исходный код
Форматирует тип для сообщений об ошибках и журналов.
include?(type, term, collection)Исходный код
@spec include?(t(), term(), Enum.t()) :: boolean()
Проверяет, содержит ли collection значение term.
В зависимости от указанного type выполняется структурное или семантическое сравнение.
Примеры
iex> include?(:integer, 1, 1..3)
true
iex> include?(:decimal, Decimal.new("1"), [Decimal.new("1.00"), Decimal.new("2.00")])
true load(type, value, loader \\ &load/2)Исходный код
@spec load(t(), term(), (t(), term() -> {:ok, term()} | :error)) ::
{:ok, term()} | :error Загружает значение с указанным типом.
iex> load(:string, nil)
{:ok, nil}
iex> load(:string, "foo")
{:ok, "foo"}
iex> load(:integer, 1)
{:ok, 1}
iex> load(:integer, "10")
:error match?(schema_type, query_type)Исходный код
@spec match?(t(), primitive()) :: boolean()
Проверяет, соответствует ли заданный тип примитивному типу, который может использоваться в запросах.
iex> match?(:string, :any)
true
iex> match?(:any, :string)
true
iex> match?(:string, :string)
true
iex> match?({:array, :string}, {:array, :any})
true
iex> match?(Ecto.UUID, :uuid)
true
iex> match?(Ecto.UUID, :string)
false parameterized?(arg1, module)Исходный код
@spec parameterized?(t(), module()) :: boolean()
Проверяет, параметризован ли заданный тип указанным модулем.
iex> type = Ecto.ParameterizedType.init(Ecto.Enum, values: [a: 1]) iex> Ecto.Type.parameterized?(type, Ecto.Enum) true iex> Ecto.Type.parameterized?(type, MyEnum) false
primitive?(base)Исходный код
@spec primitive?(t()) :: boolean()
Проверяет, является ли тип примитивным.
iex> primitive?(:string)
true
iex> primitive?(Another)
false
iex> primitive?({:array, :string})
true
iex> primitive?({:array, Another})
true trim(type, value)Исходный код
Обрезает значение в соответствии с его типом.
В настоящее время обрезаются все строки, кроме строк типа :binary.
type(type)Исходный код
@spec type(t()) :: t()
Возвращает базовый тип схемы для заданного, возможно пользовательского, типа.
iex> type(:string)
:string
iex> type(Ecto.UUID)
:uuid
iex> type({:array, :string})
{:array, :string}
iex> type({:array, Ecto.UUID})
{:array, :uuid}
iex> type({:map, Ecto.UUID})
{:map, :uuid}
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Type.html