View Source TypedEctoSchema (typed_ecto_schema v0.4.1)
TypedEctoSchema provides a DSL on top of Ecto.Schema
to define schemas with typespecs without
all the boilerplate code.
rationale
Rationale
Normally, when defining an Ecto.Schema
you probably want to define:
- the schema itself
- the list of enforced keys (which helps reducing problems)
- its associated type (
Ecto.Schema
doesn't define it for you)
It ends up in something like this:
defmodule Person do
use Ecto.Schema
@enforce_keys [:name]
schema "people" do
field(:name, :string)
field(:age, :integer)
field(:happy, :boolean, default: true)
field(:phone, :string)
belongs_to(:company, Company)
timestamps(type: :naive_datetime_usec)
end
@type t() :: %__MODULE__{
__meta__: Ecto.Schema.Metadata.t(),
id: integer() | nil,
name: String.t(),
age: non_neg_integer() | nil,
happy: boolean(),
phone: String.t() | nil,
company_id: integer() | nil,
company: Company.t() | Ecto.Association.NotLoaded.t() | nil,
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
}
end
This is problematic for a a lot of reasons, summing up:
- A lot of repetition. Field names appear in 3 different places, so in order to understand one field, a reader needs to go up and down the code to get that.
- Ecto has some "hidden" fields that are added behind the scenes to the struct, such as the
primary key
id
, the foreign keycompany_id
, the timestamps and the__meta__
field for schemas. Knowing all those rules can be hard to remember and would probably be easily forgotten when changing the schema. Also, Ecto has strange types for associations and metadata that need to be remembered.
All of this makes this process extremely repetitive and error prone. Sometimes, you want to
enforce factory functions to control defaults in a better way, you would probably add all fields
to @enforce_keys
. This would make the @enforce_keys
big and repetitive, once again.
This module aims to help with that, by providing some syntax sugar that allow you to define this in a more compact way.
defmodule Person do
use TypedEctoSchema
typed_schema "people" do
field(:name, :string, enforce: true, null: false)
field(:age, :integer) :: non_neg_integer() | nil
field(:happy, :boolean, default: true, null: false)
field(:phone, :string)
belongs_to(:company, Company)
timestamps(type: :naive_datetime_usec)
end
end
This is way simpler and less error prone. There is a lot going under the hoods here.
extra-options
Extra Options
All ecto macros are called under the hood with the options you pass, with exception of a few added options:
:null
- whentrue
, adds a| nil
to the typespec. Default istrue
. Has no effect onhas_one/3
because it can always benil
. Onbelongs_to/3
only add| nil
to the underlying foreign key.:enforce
- whentrue
adds the field to the@enforce_keys
. Default isfalse
schema-options
Schema Options
When calling typed_schema/3
or typed_embedded_schema/2
you can pass some options, as
defined:
:null
- Set the default:null
field option, which normally is true. Note that it is still can be overwritten by passing:null
to the field itself. Also,embeds_many
andhas_many
can never be null, because they are always initialized to empty string, so they never receive the| nil
on the typespec. In addition to that,has_one/3
andbelongs_to/3
always receive| nil
because the related schema may be deleted from the repo so it is safe to always assume they can benil
.:enforce
- Whentrue
, enforces all fields unless they explicitly setenforce: false
or defines a default (default: value
), since it makes no sense to have a default value for an enforced field.:opaque
- Whentrue
makes the generated typet
be an opaque type.
type-inference
Type Inference
TypedEctoSchema does it's best job to guess the typespec for the field. It does so by following
the Elixir types as defined in Ecto.Schema
.
For custom Ecto.Type
and related schemas (embedded and associations), which are always a
module, it assumes the schemas has a type t/0
defined, so for a schema called MySchema
, it
will assume the type is MySchema.t/0
, which is also, the default type generated by this
library.
overriding-the-typespec-for-a-field
Overriding the Typespec for a field
If for somereason you want to narrow the type or the automatic type inference is incorrect,
the ::
operator allows the typespec to be overriden.
This is done as you would when defining typespecs.
So, for example, instead of
field(:my_int, :integer)
Which would generate a integer() | nil
typespec, you can:
field(:my_int, :integer) :: non_neg_integer() | nil
And then have a non_neg_integer()
type for it.
non-explicit-generated-fields
Non explicit generated fields
Ecto generates some fields for you in a lot of cases, they are:
- For primary keys
- When using a
belongs_to/3
- When calling
timestamps/1
The __meta__
typespec is automatically generated and cannot be overriden. That is because
there is no point on overriding it.
primary-keys
Primary Keys
Primary keys are generated by default and can be customized by the @primary_key
module
attribute, just as defined by Ecto. We handle @primary_key
the same way we handle field/3
, so you
can pass the same field options to it.
However, if you want to customize the type, you need to set @primary_key false
and define a
field with primary_key: true
.
belongs-to
Belongs To
belongs_to
generates an underlying foreign key that is dependent on a few Ecto options, as
defined on Ecto.Schema
.
The options we are interested in are :foreign_key
, :define_field
and :type
When :null
is passed, it will add | nil
to the generated foreign_key
's typespec.
The :enforce
option enforces the association field instead.
If you want to :enforce
the foreign key to be set, you should probably pass define_field: false
and define the foreign key by hand, setting another field/3
, the same way as
described by Ecto's doc.
timestamps
Timestamps
In the case of the timestamps, we currently don't allow overriding the type by using the ::
operator.
That being said, however, we define the type of the fields using the :type
option
(as defined by Ecto doc)
Link to this section Summary
Link to this section Functions
Replaces Ecto.Schema.embedded_schema/1
Replaces Ecto.Schema.schema/2