Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Yurii Bodarev - Ecto DSL

109 views

Published on

Elixir Club Ternopil - March 25, 2017
Friendly Restoration "Mamont"

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Yurii Bodarev - Ecto DSL

  1. 1. Ecto DSL Domain specific language for writing queries and interacting with databases in Elixir.
  2. 2. Yurii Bodarev Back-end software developer twitter.com/bodarev_yurii github.com/yuriibodarev
  3. 3. Ecto official resources github.com/elixir-ecto/ecto hexdocs.pm/ecto/Ecto.html pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0
  4. 4. 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
  5. 5. Basic data types and structures • Atoms • Lists • Tuples
  6. 6. Atoms Atoms are constants where their name is their own value. iex> :hello :hello iex> :hello == :world false iex> true == :true true
  7. 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
  8. 8. 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]
  9. 9. 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]) []
  10. 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
  11. 11. Associative data structures • Keyword lists • Maps
  12. 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. 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. 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. 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. 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. 17. Example of the Ecto query query = from w in Weather, where: w.prcp > 0, where: w.temp < 20, select: w
  18. 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. 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. 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"}
  21. 21. 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"}
  22. 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. 23. Pattern matching iex> {a, b, c} = {:hello, "world", 42} {:hello, "world", 42} iex> a :hello iex> b "world" iex> c 42
  24. 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. 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. 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. 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. 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. 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. 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. 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. 32. Ecto playground Ecto in not an ORM github.com/yuriibodarev/Ecto_not_ORM Requires: PostgreSQL Run within IEx console: iex -S mix
  33. 33. Repositories Via the repository, we can create, update, destroy and query existing database entries.
  34. 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. 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. 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. 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. 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. 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. 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. 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. 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
  43. 43. 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>
  44. 44. Changeset with Repository functions iex> valid_changeset.valid? true iex> Blog.Repo.insert(valid_changeset) {:ok, %Blog.User{…, id: 7, …}}
  45. 45. 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>}
  46. 46. Changeset with Repository functions case Blog.Repo.update(changeset) do {:ok, user} -> # user updated {:error, changeset} -> # an error occurred end
  47. 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. 48. Query Ecto allows you to write queries in Elixir and send them to the repository, which translates them to the underlying database.
  49. 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. 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. 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. 52. 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

×