yurii bodarev - ecto dsl
TRANSCRIPT
Ecto DSLDomain specific language for writing queries and interacting with
databases in Elixir.
Yurii Bodarev
Back-end software developer
twitter.com/bodarev_yurii
github.com/yuriibodarev
Ecto official resources
github.com/elixir-ecto/ecto
hexdocs.pm/ecto/Ecto.html
pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0
Brief contents
• Elixir Data Structures• Basic data types and structures• Associative data structures• Structs• Pattern Matching
• Ecto• Ecto.Repo• Ecto.Schema• Ecto.Changeset• Ecto.Query
Basic data types and structures
•Atoms
• Lists
• Tuples
Atoms
Atoms are constants where their name is their own value.
iex> :hello:hello
iex> :hello == :worldfalse
iex> true == :truetrue
Lists (Linked)
Lists are used to manage dynamic, variable-sized collections of data of any type.
iex> [1, "abc", true, 3] [1, "abc", true, 3]
iex> length([1, 2, 3])
3
List - recursive structure
[head | tail]
iex> [1 | [2, 3, 4]]
[1, 2, 3, 4]
[head | [head | [head | tail…]]]
iex> [1 | [2 | [3 | [4 | []]]]]
[1, 2, 3, 4]
List - recursive structure
iex> hd([1, 2, 3, 4])
1
iex> tl([1, 2, 3, 4])
[2, 3, 4]
iex> tl([2, 3, 4])
[3, 4]
iex> tl([4])
[]
Tuples
Tuples are untyped structures often used to group a fixed number of elements together.
iex> tuple = {:ok, "hello"} {:ok, "hello"}
iex> elem(tuple, 1) "hello"
iex> tuple_size(tuple) 2
Associative data structures
•Keyword lists
•Maps
List as key-value data structure
It is common to use a list of 2-item tuples as the representation of a key-value data structure
iex> list = [{"a", 1}, {"b", 2}, {"c", 3}]
[{"a", 1}, {"b", 2}, {"c", 3}]
iex> List.keyfind(list, "b", 0)
{"b", 2}
Keyword lists
When we have a list of tuples and the first item of the tuple (i.e. the key) is an atom, we call it a keyword list.
iex> [{:a, 1}, {:b, 2}, {:c, 3}]
[a: 1, b: 2, c: 3]
Keyword lists
Elixir supports a special syntax for defining such lists: [key: value]
iex> [a: 1, b: 2, c: 3] == [{:a, 1}, {:b, 2}, {:c, 3}]
true
• Keys must be atoms.
• Keys are ordered, as specified by the developer.
• Keys can be given more than once.
We can use all operations available to lists on keyword lists
iex> list = [a: 1, c: 3, b: 2]
[a: 1, c: 3, b: 2]
iex> hd(list)
{:a, 1}
iex> tl(list)
[c: 3, b: 2]
iex> list[:a]
1
iex> newlist = [a: 0] ++ list
[a: 0, a: 1, c: 3, b: 2]
iex> newlist[:a]
0
iex> list[:d]
nil
Keyword lists - default mechanism for passing options to functions in Elixir
iex> if true, do: "THIS""THIS"
iex> if false, do: "THIS", else: "THAT""THAT"
iex> if(false, [do: "THIS", else: "THAT"]) "THAT"
When the keyword list is the last argument of a function, the square brackets are optional.
Example of the Ecto query
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w
Maps
A map is a key-value store, where keys and values can be any term. A map is created using the %{} syntax. Maps’ keys do not follow developer ordering.
iex> map = %{:a => 1, 2 => "b", "c" => 3}
%{2 => "b", :a => 1, "c" => 3}
iex> map[:a]
1
iex> map[2]
"b"
iex> map["d"]
nil
MapsWhen all the keys in a map are atoms, you can use the keyword syntax.
iex> map = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}
iex> map.a
1
iex> map.d
** (KeyError) key :d not found in: %{a: 1, b: 2, c: 3}
iex> %{map | c: 5}
%{a: 1, b: 2, c: 5}
iex> %{map | d: 0}
** (KeyError) key :d not found in: %{a: 1, b: 2, c: 3}
Structs
Structs are extensions built on top of maps that provide compile-time checks and default values.
iex> defmodule User do
...> defstruct name: "Ivan", age: 25
...> end
iex> %User{}
%User{age: 25, name: "Ivan"}
Structs
iex> %User{name: "Maria"}
%User{age: 25, name: "Maria"}
iex> %User{other: "Something"}
** (KeyError) key :other not found in: %User{age:25, name: "Ivan"}
Structs are bare maps underneath, but none of the protocols implemented for maps are available for structs
iex> ivan = %User{}
%User{age: 25, name: "Ivan"}
iex> is_map(ivan)
true
iex> Map.keys(ivan)
[:__struct__, :age, :name]
iex> ivan.__struct__
User
iex> ivan[:age]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does notimplement the Access behaviour)
Pattern matching
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a :helloiex> b "world"iex> c 42
A pattern match will error if the sides can’t be matchediex> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value:{:hello, "world"}
iex> {a, b, c} = [:hello, "world", 42]
** (MatchError) no match of right hand side value:[:hello, "world", 42]
We can match on specific values
iex> {:ok, result} = {:ok, 13}
{:ok, 13}
iex> result
13
iex> {:ok, result} = {:error, "Not Found!"}
** (MatchError) no match of right hand side value:{:error, "Not Found!"}
We can match on specific values
post = Repo.get!(Post, 42)
case Repo.delete post do
{:ok, struct} -> # Deleted with success
{:error, changeset} -> # Something went wrong
end
Pattern match on lists
iex> [a, b, c] = [1, 2, 3]
[1, 2, 3]
iex> b
2
iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]
iex> [] = [1, 2, 3]
** (MatchError) no match of right hand side value: [1, 2, 3]
Pattern match on keyword lists
iex> [a: a] = [a: 1]
[a: 1]
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
Pattern match on maps
iex> %{} = %{a: 1, b: 2}%{a: 1, b: 2}
iex> %{b: b} = %{a: 1, b: 2}%{a: 1, b: 2}
iex> b 2
iex> %{c: c} = %{a: 1, b: 2}** (MatchError) no match of right hand side value: %{a: 1, b: 2}
The pin ^ operator and _iex> x = 2
2
iex> {1, ^x} = {1, 2}
{1, 2}
iex> {a, _} = {1, 2}
{1, 2}
iex> a
1
Ecto
Ecto is split into 4 main components:
• Ecto.Repo - repositories are wrappers around the data store.
• Ecto.Schema - schemas are used to map any data source into an Elixir struct.
• Ecto.Changeset - allow developers to filter, cast, and validate changes before we apply them to the data.
• Ecto.Query - written in Elixir syntax, queries are used to retrieve information from a given repository.
Ecto playground
Ecto in not an ORM
github.com/yuriibodarev/Ecto_not_ORM
Requires: PostgreSQL
Run within IEx console: iex -S mix
Repositories
Via the repository, we can create, update, destroy and query existing database entries.
Repositories
Ecto.Repo is a wrapper around the database. We can define a repository as follows (lib\blog\repo.ex):
defmodule Blog.Repo do
use Ecto.Repo, otp_app: :blog
end
Repositories
A repository needs an adapter and credentials to communicate to the database. Configuration for the Repo usually defined in your config/config.exs:
config :blog, Blog.Repo,
adapter: Ecto.Adapters.Postgres,
database: "blog_repo",
username: "postgres",
password: "postgres",
hostname: "localhost"
RepositoriesEach repository in Ecto defines a start_link/0. Usually this function is invoked as part of your application supervision tree (lib\blog.ex):
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [ worker(Blog.Repo, []), ]
opts = [strategy: :one_for_one, name: Blog.Supervisor]
Supervisor.start_link(children, opts)
end
Schema
Schemas allows developers to define the shape of their data. (lib\blog\user.ex)
defmodule Blog.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :reputation, :integer, default: 0
has_many :posts, Blog.Post, on_delete: :delete_all
timestamps
end
end
Schema
By defining a schema, Ecto automatically defines a struct:
iex> user = %Blog.User{name: "Bill"}
%Blog.User{__meta__: #Ecto.Schema.Metadata<:built, "users">, id: nil, inserted_at: nil, name: "Bill"}, posts: #Ecto.Association.NotLoaded<association :posts is not loaded>, reputation: 0, updated_at:nil}
Schema
Using Schema we can interact with a repository:
iex> user = %Blog.User{name: "Bill", reputation: 10}
%Blog.User{…}
iex> Blog.Repo.insert!(user)
%Blog.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 6, inserted_at: ~N[2016-12-13 16:16:35.983000], name: "Bill", posts:#Ecto.Association.NotLoaded<association :posts is not loaded>, reputation: 10, updated_at: ~N[2016-12-13 16:16:36.001000]}
Schema
# Get the user back
iex> newuser = Blog.Repo.get(Blog.User, 6)
iex> newuser.id
6
# Delete it
iex> Blog.Repo.delete(newuser)
{:ok, %Blog.User{…, id: 6,…}}
Schema
We can use pattern matching on Structs created with Schemas:
iex> %{name: name, reputation: reputation} =...> Blog.Repo.get(Blog.User, 1)
iex> name
"Alex"
iex> reputation
144
Changesets
We can add changesets to our schemas to validate changes before we apply them to the data (lib\blog\user.ex):
def changeset(user, params \\ %{}) do
user
|> cast(params, [:name, :reputation])
|> validate_required([:name, :reputation])
|> validate_inclusion(:reputation, -999..999)
end
Changesets
iex> alina = %Blog.User{name: "Alina"}
iex> correct_changeset = Blog.User.changeset(alina, %{reputation: 55})
#Ecto.Changeset<action: nil, changes: %{reputation: 55}, errors: [], data: #Blog.User<>, valid?: true>
iex> invalid_changeset = Blog.User.changeset(alina, %{reputation: 1055})
#Ecto.Changeset<action: nil, changes: %{reputation: 1055}, errors:[reputation: {"is invalid", [validation: :inclusion]}], data:#Blog.User<>, valid?: false>
Changeset with Repository functions
iex> valid_changeset.valid?
true
iex> Blog.Repo.insert(valid_changeset)
{:ok, %Blog.User{…, id: 7, …}}
Changeset with Repository functions
iex> invalid_changeset.valid?
false
iex> Blog.Repo.insert(invalid_changeset)
{:error, #Ecto.Changeset<action: :insert, changes: %{reputation: 1055}, errors: [reputation: {"is invalid", [validation: :inclusion]}], data:#Blog.User<>, valid?: false>}
Changeset with Repository functions
case Blog.Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
We can provide different changeset functions for different use cases
def registration_changeset(user, params) do
# Changeset on create
end
def update_changeset(user, params) do
# Changeset on update
end
Query
Ecto allows you to write queries in Elixir and send them to the repository, which translates them to the underlying database.
Query using predefined Schema
# Query using predefined Schemaquery = from u in User,
where: u.reputation > 35, select: u
# Returns %User{} structs matching the queryRepo.all(query)
[%Blog.User{…, id: 2, …, name: "Bender", …, reputation: 42, …},
%Blog.User{…, id: 1, …, name: "Alex", …, reputation: 144, …}]
Directly querying the “users” table
# Directly querying the “users” table
query = from u in "users",
where: u.reputation > 30,
select: %{name: u.name, reputation: u.reputation}
# Returns maps as defined in select
Repo.all(query)
[%{name: "Bender", reputation: 42}, %{name: "Alex", reputation: 144}]
External values in Queries
# ^ operator
min = 33
query = from u in "users",
where: u.reputation > ^min,
select: u.name
# casting
mins = "33"
query = from u in "users",
where: u.reputation > type(^mins, :integer),
select: u.name
External values in Queries
If the query is made against Schema than Ecto will automatically cast external value
min = "35"
Repo.all(from u in User, where: u.reputation > ^min)
You can also skip Select to retrieve all fields specified in the Schema