yurii bodarev - ecto dsl

52
Ecto DSL Domain specific language for writing queries and interacting with databases in Elixir.

Upload: elixir-meetup

Post on 05-Apr-2017

40 views

Category:

Technology


1 download

TRANSCRIPT

Page 1: Yurii Bodarev - Ecto DSL

Ecto DSLDomain specific language for writing queries and interacting with

databases in Elixir.

Page 2: Yurii Bodarev - Ecto DSL

Yurii Bodarev

Back-end software developer

twitter.com/bodarev_yurii

github.com/yuriibodarev

Page 3: Yurii Bodarev - Ecto DSL

Ecto official resources

github.com/elixir-ecto/ecto

hexdocs.pm/ecto/Ecto.html

pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0

Page 4: Yurii Bodarev - Ecto DSL

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

Page 5: Yurii Bodarev - Ecto DSL

Basic data types and structures

•Atoms

• Lists

• Tuples

Page 6: Yurii Bodarev - Ecto DSL

Atoms

Atoms are constants where their name is their own value.

iex> :hello:hello

iex> :hello == :worldfalse

iex> true == :truetrue

Page 7: Yurii Bodarev - Ecto DSL

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

Page 8: Yurii Bodarev - Ecto DSL

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]

Page 9: Yurii Bodarev - Ecto DSL

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])

[]

Page 10: Yurii Bodarev - Ecto DSL

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

Page 11: Yurii Bodarev - Ecto DSL

Associative data structures

•Keyword lists

•Maps

Page 12: Yurii Bodarev - Ecto DSL

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}

Page 13: Yurii Bodarev - Ecto DSL

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]

Page 14: Yurii Bodarev - Ecto DSL

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.

Page 15: Yurii Bodarev - Ecto DSL

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

Page 16: Yurii Bodarev - Ecto DSL

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.

Page 17: Yurii Bodarev - Ecto DSL

Example of the Ecto query

query = from w in Weather,

where: w.prcp > 0,

where: w.temp < 20,

select: w

Page 18: Yurii Bodarev - Ecto DSL

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

Page 19: Yurii Bodarev - Ecto DSL

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}

Page 20: Yurii Bodarev - Ecto DSL

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"}

Page 21: Yurii Bodarev - Ecto DSL

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"}

Page 22: Yurii Bodarev - Ecto DSL

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)

Page 23: Yurii Bodarev - Ecto DSL

Pattern matching

iex> {a, b, c} = {:hello, "world", 42}

{:hello, "world", 42}

iex> a :helloiex> b "world"iex> c 42

Page 24: Yurii Bodarev - Ecto DSL

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]

Page 25: Yurii Bodarev - Ecto DSL

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!"}

Page 26: Yurii Bodarev - Ecto DSL

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

Page 27: Yurii Bodarev - Ecto DSL

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]

Page 28: Yurii Bodarev - Ecto DSL

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]

Page 29: Yurii Bodarev - Ecto DSL

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}

Page 30: Yurii Bodarev - Ecto DSL

The pin ^ operator and _iex> x = 2

2

iex> {1, ^x} = {1, 2}

{1, 2}

iex> {a, _} = {1, 2}

{1, 2}

iex> a

1

Page 31: Yurii Bodarev - Ecto DSL

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.

Page 32: Yurii Bodarev - Ecto DSL

Ecto playground

Ecto in not an ORM

github.com/yuriibodarev/Ecto_not_ORM

Requires: PostgreSQL

Run within IEx console: iex -S mix

Page 33: Yurii Bodarev - Ecto DSL

Repositories

Via the repository, we can create, update, destroy and query existing database entries.

Page 34: Yurii Bodarev - Ecto DSL

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

Page 35: Yurii Bodarev - Ecto DSL

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"

Page 36: Yurii Bodarev - Ecto DSL

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

Page 37: Yurii Bodarev - Ecto DSL

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

Page 38: Yurii Bodarev - Ecto DSL

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}

Page 39: Yurii Bodarev - Ecto DSL

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]}

Page 40: Yurii Bodarev - Ecto DSL

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,…}}

Page 41: Yurii Bodarev - Ecto DSL

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

Page 42: Yurii Bodarev - Ecto DSL

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

Page 43: Yurii Bodarev - Ecto DSL

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>

Page 44: Yurii Bodarev - Ecto DSL

Changeset with Repository functions

iex> valid_changeset.valid?

true

iex> Blog.Repo.insert(valid_changeset)

{:ok, %Blog.User{…, id: 7, …}}

Page 45: Yurii Bodarev - Ecto DSL

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>}

Page 46: Yurii Bodarev - Ecto DSL

Changeset with Repository functions

case Blog.Repo.update(changeset) do

{:ok, user} ->

# user updated

{:error, changeset} ->

# an error occurred

end

Page 47: Yurii Bodarev - Ecto DSL

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

Page 48: Yurii Bodarev - Ecto DSL

Query

Ecto allows you to write queries in Elixir and send them to the repository, which translates them to the underlying database.

Page 49: Yurii Bodarev - Ecto DSL

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, …}]

Page 50: Yurii Bodarev - Ecto DSL

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}]

Page 51: Yurii Bodarev - Ecto DSL

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

Page 52: Yurii Bodarev - Ecto DSL

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