Elixir has a powerful and rapidly growing web development framework Phoenix and a part of this framework is an Ecto, a DSL library for data access. This talk will be an introduction into Ecto revealing the basic concepts and some of its more advanced features.
Elixir Meetup 4 Lviv
6. Atoms
Atoms are constants where their name is their own value.
iex> :hello
:hello
iex> :hello == :world
false
iex> true == :true
true
7. 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
10. 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
12. 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}
13. 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]
14. 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.
15. 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
16. 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.
17. Example of the Ecto query
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w
18. 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
19. Maps
When 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}
20. 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"}
22. 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 not
implement the Access behaviour)
23. Pattern matching
iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"
iex> c
42
24. A pattern match will error if the sides can’t be
matched
iex> {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]
25. 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!"}
26. 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
27. 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]
28. 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]
29. 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}
30. The pin ^ operator and _
iex> x = 2
2
iex> {1, ^x} = {1, 2}
{1, 2}
iex> {a, _} = {1, 2}
{1, 2}
iex> a
1
31. 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.
32. Ecto playground
Ecto in not an ORM
github.com/yuriibodarev/Ecto_not_ORM
Requires: PostgreSQL
Run within IEx console: iex -S mix
34. Repositories
Ecto.Repo is a wrapper around the database. We can define a
repository as follows (libblogrepo.ex):
defmodule Blog.Repo do
use Ecto.Repo, otp_app: :blog
end
35. 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"
36. Repositories
Each repository in Ecto defines a start_link/0. Usually this function is invoked as part
of your application supervision tree (libblog.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
37. Schema
Schemas allows developers to define the shape of their data. (libbloguser.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
38. 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}
39. 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]}
40. 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,…}}
41. 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
42. Changesets
We can add changesets to our schemas to validate changes before we
apply them to the data (libbloguser.ex):
def changeset(user, params %{}) do
user
|> cast(params, [:name, :reputation])
|> validate_required([:name, :reputation])
|> validate_inclusion(:reputation, -999..999)
end
46. Changeset with Repository functions
case Blog.Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
47. 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
48. Query
Ecto allows you to write queries in Elixir and send them to the
repository, which translates them to the underlying database.
49. Query using predefined Schema
# Query using predefined Schema
query = from u in User,
where: u.reputation > 35,
select: u
# Returns %User{} structs matching the query
Repo.all(query)
[%Blog.User{…, id: 2, …, name: "Bender", …, reputation: 42, …},
%Blog.User{…, id: 1, …, name: "Alex", …, reputation: 144, …}]
50. 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}]
51. 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
52. External values in Queries
If query is created with predefined 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
53. Associations
schema "users" do
field :name, :string
field :reputation, :integer, default: 0
has_many :posts, Blog.Post, on_delete: :delete_all
timestamps
end
54. Associations
alex = Repo.get_by(User, name: "Alex")
%Blog.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
inserted_at: ~N[2016-12-17 06:36:54.916000], name: "Alex",
posts: #Ecto.Association.NotLoaded<association :posts is not loaded>,
reputation: 13, updated_at: ~N[2016-12-17 06:36:54.923000]}
57. Queries Composition
alex = Repo.get_by(User, name: "Alex")
alex_post = from p in Post,
where: p.user_id == ^alex.id
alex_pin = from ap in alex_post,
where: ap.pinned == true