Ecto: From the Ground Up
ElixirConf 2019
Darin Wilson / Eric Meadows-Jönsson
Class Exercises
github.com/darinwilson/music_db_workshop
hexdocs.pm/ecto
Ecto
-
Repo
-
Query
-
Schema
-
Changeset
-
Multi
Repo
The Repository Pattern
artist = Artist.get(1)
artist.name = "Miles Davis"
artist.save
Active Record Pattern
The Repository Pattern
-
Centralized class/module connects to DB
-
Database operations sent to the Repository
artist = Repo.get(Artist, 1)
changeset =
Ecto.Changeset.change(artist, name: "Miles Davis")
Repo.update(changeset)
Repository Pattern
artist = Repo.get(Artist, 1)
changeset =
Ecto.Changeset.change(artist,
name: "Miles Davis")
Repo.update(changeset)
Active Record
artist = Artist.get(1)
artist.name = "Miles Davis"
artist.save
Repository
artist = Repo.get(Artist, 1)
changeset =
Ecto.Changeset.change(artist,
name: "Miles Davis")
Repo.update(changeset)
Active Record
artist = Artist.get(1)
artist.name = "Miles Davis"
artist.save
Repository
artist = Repo.get(Artist, 1)
changeset =
Ecto.Changeset.change(artist,
name: "Miles Davis")
Repo.update(changeset)
Active Record
artist = Artist.get(1)
artist.name = "Miles Davis"
artist.save
Repository
# lib/my_app/repo.ex
defmodule MyApp.Repo
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
end
Typical Use
# lib/my_app/repo.ex
defmodule MyApp.Repo
use Ecto.Repo,
otp_app: :my_app
adapter: Ecto.Adapters.Postgres,
end
# config/config.exs
config :my_app, MyApp.Repo,
database: "my_app_db",
username: "postgres",
password: "postgres",
hostname: "localhost"
Typical Use
Work directly with the data
The *all functions
insert_all
update_all
delete_all
all
alias MyApp.Repo
Repo.insert_all("artists", [[name: "John Coltrane"]])
=> {1, nil}
Inserts
Repo.insert_all("artists", [[name: "John Coltrane"]])
=> {1, nil}
Repo.insert_all("artists", [[name: "John Coltrane"],
[name: "Max Roach"]])
=> {2, nil}
Inserts
Repo.insert_all("artists", [[name: "John Coltrane"]])
=> {1, nil}
Repo.insert_all("artists", [[name: "John Coltrane"],
[name: "Max Roach"]])
=> {2, nil}
Repo.insert_all("artists", [%{name: "John Coltrane"},
%{name: "Max Roach"}])
=> {2, nil}
Inserts
Repo.insert_all("artists", [[name: "John Coltrane"]],
returning: [:id, :name])
Inserts With A Return Value
Repo.insert_all("artists", [[name: "John Coltrane"]],
returning: [:id, :name])
=> {1, [%{id: 4, name: "John Coltrane"}]}
Inserts With A Return Value
Repo.update_all("artists",
set: [updated_at: Ecto.DateTime.utc])
=> {75, nil}
Updates
Repo.delete_all("artists")
=> {75, nil}
Deletes
Ecto.Adapters.SQL.query(Repo,
"select * from artists where id=$1", [1])
=> {:ok,
%Postgrex.Result{columns: ["id", "name", "inserted_at", "updated_at"],
command: :select, connection_id: 10467, num_rows: 1,
rows: [[1, "Miles Davis", {{2016, 8, 25}, {7, 20, 34, 0}},
{{2016, 8, 25}, {7, 20, 34, 0}}]]}}
Queries
Customize Your Repo
# get the highest id of a table
Repo.aggregate("artists", :max, :id)
#=> 3
Customize Your Repo
defmodule MusicDB.Repo do
use Ecto.Repo, otp_app: :music_db
def max_id(table_name) do
aggregate(table_name, :max, :id)
end
end
Customize Your Repo
defmodule MusicDB.Repo do
use Ecto.Repo, otp_app: :music_db
def max_id(table_name) do
aggregate(table_name, :max, :id)
end
end
# now we can do:
Repo.max_id("artists")
Customize Your Repo
https://bit.ly/2ZovyXZ
Repo.insert_all("artists", [[name: "John Coltrane"]])
Repo.update_all("artists",
set: [updated_at: Ecto.DateTime.utc])
Repo.delete_all("artists")
Exercises
Queries
Queries
The Basics
SELECT t.id, t.title, a.title
FROM tracks t
JOIN albums a ON t.album_id = a.id
WHERE t.duration > 900;
import Ecto.Query
query = from t in "tracks",
join: a in "albums", on: t.album_id == a.id,
where: t.duration > 900,
select: [t.id, t.title, a.title]
The Basics
SELECT t.id, t.title, a.title
FROM tracks t
JOIN albums a ON t.album_id = a.id
WHERE t.duration > 900;
import Ecto.Query
query =
"tracks"
|> join(:inner, [t], a in "albums", t.album_id == a.id)
|> where([t,a], t.duration > 900)
|> select([t,a], [t.id, t.title, a.title])
Inspecting SQL
query = from "artists", select: [:name]
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
#=> {"SELECT a0.\"name\" FROM \"artists\" AS a0", []}
Inspecting SQL
query = from "artists", select: [:name]
Repo.all(query)
#=> [["Miles Davis"], ["Bill Evans"], ["Bobby Hutcherson"]]
Start with Where
q = from "artists",
where: [name: "Bill Evans"],
select: [:id, :name]
Repo.all(q)
#=> [[2, "Bill Evans"]]
# What if we want to dynamically set the name?
artist_name = "Taylor Swift"
q = from "artists",
where: [name: ^artist_name],
select: [:id, :name]
Query Bindings
# This doesn't work
q = from "artists", where: name == "Bill Evans", select: [:id, :name]
# Now this works :)
q = from a in "artists", where: a.name == "Bill Evans", select: [:id, :name]
Query Bindings
# like statements
q = from a in "artists", where: like(a.name, "Miles%"), select: [:id, :name]
# checking for null
q = from a in "artists", where: is_nil(a.name), select: [:id, :name]
# checking for not null
q = from a in "artists", where: not is_nil(a.name), select: [:id, :name]
# date comparision - this finds artists added more than 1 year ago
q = from a in "artists", where: a.inserted_at < ago(1, "year"), select: [:id, :name]
Fragments
q = from a in "artists",
where: fragment("lower(?)", a.name) == "miles davis", select: [:id, :name]
Ecto.Adapters.SQL.to_sql(:all, Repo, q)
#=> {"SELECT a0.\"id\", a0.\"name\" FROM \"artists\" AS a0
#=> WHERE (lower(a0.\"name\") = 'miles davis')",
Ordering & Grouping
# Select all artists names ordered alphabetically
q = from a in "artists", select: [a.name], order_by: a.name
Repo.all(q)
#=> [["Bill Evans"], ["Bobby Hutcherson"], ["Miles Davis"]]
# Order by results descending.
q = from a in "artists", select: [a.name], order_by: [desc: a.name]
Repo.all(q)
#=> [["Miles Davis"], ["Bobby Hutcherson"], ["Bill Evans"]]
Ordering & Grouping
# Grouping tracks by album and summing their times played
q = from t in "tracks", select: [t.album_id, sum(t.number_of_plays)],
group_by: t.album_id
Repo.all(q)
#=> [[4, 2540], [1, 2619], [5, 3057], [3, 2528], [2, 4491]]
Joins
Joins
# Select all tracks that have a duration longer than 15 minutes.
q = from t in "tracks",
join: a in "albums", on: t.album_id == a.id,
where: t.duration > 900,
select: [a.title, t.title]
Repo.all(q)
#=> [["Cookin' At The Plugged Nickel", "No Blues"],
#=> ["Cookin' At The Plugged Nickel", "If I Were A Bell"]]
Joins
# You can use the join keyword more than once to join in multiple tables!
q = from t in "tracks",
join: a in "albums", on: t.album_id == a.id,
join: ar in "artists", on: a.artist_id == ar.id,
where: t.duration > 900,
select: %{album: a.title, track: t.title, artist: ar.name}
Repo.all(q)
#=> [%{album: "Cookin' At The Plugged Nickel", artist: "Miles Davis",
#=> track: "If I Were A Bell"},
#=> %{album: "Cookin' At The Plugged Nickel", artist: "Miles Davis",
#=> track: "No Blues"},
Composing Queries
# First query to get all albums by Miles Davis.
q = from a in "albums",
join: ar in "artists", on: a.artist_id == ar.id,
where: ar.name == "Miles Davis",
select: [a.title]
Repo.all(q)
#=> [["Cookin' At The Plugged Nickel"], ["Kind Of Blue"]]
# Then we want to list all the tracks on Miles Davis's albums
q = from a in "albums",
join: ar in "artists", on: a.artist_id == ar.id,
join: t in "tracks", on: t.album_id == a.id,
where: ar.name == "Miles Davis",
select: [t.title]
# Reuse the first query!
albums_by_miles = from a in "albums",
join: ar in "artists", on: a.artist_id == ar.id,
where: ar.name == "Miles Davis"
q = from [a,ar] in albums_by_miles,
join: t in "tracks", on: t.album_id == a.id,
select: [t.title]
Composing Queries
# You can also use functions to compose queries.
def albums_by_artist(artist_name) do
from a in "albums",
join: ar in "artists", on: a.artist_id == ar.id,
where: ar.name == ^artist_name
end
def with_tracks_longer_than(query, duration) do
from a in query,
join: t in "tracks", on: t.album_id == a.id,
where: t.duration > ^duration,
distinct: true
end
q =
albums_by_artist("Taylor Swift")
|> with_tracks_longer_than(720)
Other Uses for Queries
# Update tracks with the title of "Autum Leaves"
q = from t in "tracks", where: t.title == "Autum Leaves"
Repo.update_all(q, set: [title: "Autumn Leaves"])
#=> [{1, nil}]
# Delete all tracks with the title of "Autum Leaves"
from(t in "tracks", where: t.title == "Autum Leaves")
|> Repo.delete_all()
Exercise Time!
Schemas
Creating Schemas
# You can create an elixir struct for tracks separate from Ecto like so:
defmodule MusicDB.Track do
defstruct [:id, :title, :duration, :index, :number_of_plays]
end
# With Ecto, you use the schema macro to define your struct:
defmodule MusicDB.Track do
use Ecto.Schema
schema "tracks" do
field :title, :string
field :duration, :integer
field :index, :integer
field :number_of_plays, :integer
timestamps()
end
end
# We can use %MusicDB.Track{} to create new structs from the above definition.
Data Types
Ecto Type | Elixir Type |
---|---|
:id | integer |
:binary_id | binary |
:integer | integer |
:float | float |
:boolean | boolean |
:string | UTF-8 encoded string |
:binary | binary |
{:array, inner_type} | list |
:map | map |
{:map, inner_type} | map |
:decimal | Decimal |
:date | Date |
:time | Time |
:naive_datetime | NaiveDateTime |
:utc_datetime | DateTime |
Schemas in Queries
# Schema-less Query
q = from "artists", where: [name: "Taylor Swift"], select: [:name]
Repo.all(q)
#=> [["Taylor Swift"]]
# Converted to use the Artist schema
q = from Artist, where: [name: "Taylor Swift"]
Repo.all(q)
#=> [%Artist{...}]
# You no longer need to include the select - results will be loaded into %Artist{} structs.
Inserting & Deleting w/Schemas
# Schema-less insert
Repo.insert_all("artists", [[name: "John Coltrane"]])
#=> {1, nil}
# Inserting a schema
Repo.insert(%Artist{name: "John Coltrane"})
#=> {:ok, %MusicDB.Artist{__meta__: #Ecto.Schema.Metadata<:loaded, "artists">,
#=> id: 4, name: "John Coltrane", ...}
# Schema-less deletion
from(t in "tracks", where: t.title == "Autum Leaves")
|> Repo.delete_all
# Deleting a schema
track = Repo.get_by(Track, title: "Autum Leaves")
Repo.delete(track)
#=> {:ok, %MusicDB.Track{__meta__: #Ecto.Schema.Metadata<:deleted, "tracks">, #=> id: 28, title:
More Exercises!
Associations
defmodule MusicDB.Album do
use Ecto.Schema
schema "albums" do
field :title, :string
field :release_date, :date
has_many :tracks, MusicDB.Track
end
end
One-to-many
schema "tracks" do
field :title, :string
# other fields here...
belongs_to :album, MusicDB.Album
end
belongs_to
# in album.ex
schema "albums" do
# field definitions here...
has_many :tracks, MusicDB.Track
belongs_to :artist, MusicDB.Artist
end
# in artist.ex
schema "artists" do
# field definitions here...
has_many :albums, MusicDB.Album
end
Album to Artist
Nested Associations
schema "artists" do
# field definitions here...
has_many :albums, MusicDB.Album
has_many :tracks, through: [:albums, :tracks]
end
Nested Associations
Many-To-Many
Many-To-Many
# in album.ex
schema "albums" do
# field definitions here...
many_to_many :genres, MusicDB.Genre, join_through: MusicDB.AlbumGenre
end
# in genre.ex
schema "genres" do
# field definitions here...
many_to_many :albums, MusicDB.Album, join_through: MusicDB.AlbumGenre
end
# in album_genre.ex
schema "albums_genres" do
# field definitions here...
belongs_to :albums, MusicDB.Album
belongs_to :genres, MusicDB.Genre
end
Many-To-Many
# in album.ex
schema "albums" do
# field definitions here...
many_to_many :genres, MusicDB.Genre, join_through: "albums_genres"
end
# in genre.ex
schema "genres" do
# field definitions here...
many_to_many :albums, MusicDB.Album, join_through: "albums_genres"
end
Many-To-Many
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue"
)
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue"
)
album.tracks
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue"
)
album.tracks
#Ecto.Association.NotLoaded<association :tracks is
# not loaded>
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue"
)
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue",
preload: :tracks
)
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue",
) |> Repo.preload(:tracks)
Loading Associations
album = Repo.one(
from a in Album,
where: a.title == "Kind Of Blue",
)
tracks_query = Ecto.assoc(album, :tracks)
tracks = Repo.all(tracks_query)
Loading Associations
albums = Repo.all(
from a in Album,
where: a.artist_id == 1,
)
tracks_query = Ecto.assoc(albums, :tracks)
tracks = Repo.all(tracks_query)
Loading Associations
Embedded Schemas
# in album.ex
schema "albums" do
# field definitions here...
has_many :tracks, MusicDB.Track
end
# in track.ex
schema "tracks" do
# field definitions here...
belongs_to :album, MusicDB.Album
end
Handling Deletes
# in album.ex
schema "albums" do
# field definitions here...
has_many :tracks, MusicDB.Track, on_delete: :delete_all
end
# in track.ex
schema "tracks" do
# field definitions here...
belongs_to :album, MusicDB.Album
end
Handling Deletes
Inserts With Schemas
Repo.insert_all("artists", [[name: "John Coltrane"]])
#=> {1, nil}
Inserts With Schemas
Repo.insert_all("artists", [[name: "John Coltrane"]])
#=> {1, nil}
Repo.insert(%Artist{name: "John Coltrane"})
#=> {:ok, %MusicDB.Artist{ ... }}
Inserts With Schemas
Repo.insert_all("artists", [[name: "John Coltrane"]])
#=> {1, nil}
Repo.insert(%Artist{name: "John Coltrane"})
#=> {:ok, %MusicDB.Artist{ ... }}
Repo.insert!(%Artist{name: "John Coltrane"})
#=> %MusicDB.Artist{ ... }
Inserts With Schemas
Repo.insert(
%Artist{
name: "John Coltrane",
albums: [
%Album{
title: "A Love Supreme"
}
]
}
)
Nested Inserts
Repo.insert(
%Artist{
name: "John Coltrane",
albums: [
%Album{
title: "A Love Supreme",
tracks: [
%Track{title: "Part 1: Acknowledgement", index: 1},
%Track{title: "Part 2: Resolution", index: 2},
%Track{title: "Part 3: Pursuance", index: 3},
%Track{title: "Part 4: Psalm", index: 4},
],
genres: [
%Genre{name: "spiritual jazz"},
]
}
]
}
)
Nested Inserts
Exercises
New "releases" table/schema
-
album -> has_many -> releases
-
"Giant Steps" (album)
-
"Giant Steps" (original release)
-
"Giant Steps (remastered)"
-
"Giant Steps Collector's Edition"
-
etc.
-
schema "albums" do
...
has_many :tracks, MusicDB.Track, on_delete: :delete_all
belongs_to :artist, MusicDB.Artist
end
Repo.insert(
%Artist{
name: "John Coltrane",
albums: [
%Album{
title: "A Love Supreme"
}
]
}
)
albums = Repo.all(from a in Album, preload: :tracks)
Exercises
Changeset
3 Stages
-
Cast/Filter
-
Validate
-
Execute
import Ecto.Changeset
params = %{name: "Gene Harris"}
changeset =
%Artist{}
|> cast(params, [:name])
|> validate_required([:name])
case Repo.insert(changeset) do
{:ok, artist} ->
IO.puts("Record for #{artist.name} was created.")
{:error, changeset} -> IO.inspect(changeset.errors)
end
Changeset Example
Casting/Filtering
# internal data
artist = %Artist{name: "Charlie Parker"}
# external data
params = %{"name" => "Charlie Parker"}
Internal or external data?
import Ecto.Changeset
changeset = change(%Artist{name: "Charlie Parker"})
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset = change(artist)
Internal data
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset = change(artist, name: "Robert Hutcherson")
changeset.changes
#=> %{name: "Robert Hutcherson"}
Adding Changes
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset =
artist
|> change(name: "Robert Hutcherson")
|> change(birth_date: ~D[1941-01-27])
changeset.changes
#=> %{name: "Robert Hutcherson",
# birth_date: ~D[1941-01-27]}
Adding More Changes
changeset = change(artist, name: "Robert Hutcherson",
birth_date: ~D[1941-01-27])
changeset.changes
#=> %{name: "Robert Hutcherson",
# birth_date: ~D[1941-01-27]}
Adding More Changes
External Data
Changeset.change
Changeset.cast
params = %{"name" => "Charlie Parker",
"birth_date" => "1920-08-29",
"instrument" => "alto sax"}
changeset = cast(%Artist{}, params, [:name, :birth_date])
changeset.changes
#=> %{birth_date: ~D[1920-08-29], name: "Charlie Parker"}
External Data
Validating
Validating
Built-in validation functions:
- start with validate_
- always return changesets
params = %{"name" => "Thelonius Monk",
"birth_date" => "1917-10-10"}
changeset =
%Artist{}
|> cast(params, [:name, :birth_date])
|> validate_required([:name, :birth_date])
|> validate_length(:name, min: 3)
changeset.valid?
#=> true
Validating
params = %{"name" => "x"}
changeset =
%Artist{}
|> cast(params, [:name, :birth_date])
|> validate_required([:name, :birth_date])
|> validate_length(:name, min: 3)
changeset.errors
#=> [name: {"should be at least %{count} character(s)",
#=> [count: 3, validation: :length, min: 3]},
#=> birth_date: {"can't be blank", [validation: :required]}]
Validating
Custom Validations
params = %{"name" => "Thelonius Monk",
"birth_date" => "2117-10-10"}
changeset =
%Artist{}
|> cast(params, [:name, :birth_date])
|> validate_change(:birth_date, fn :birth_date, birth_date ->
cond do
is_nil(birth_date) -> []
Date.compare(birth_date, Date.utc_today()) == :lt -> []
true -> [birth_date: "must be in the past"]
end
end)
changeset.errors
#=> [birth_date: {"must be in the past", []}]
Custom Validations
def validate_in_the_past(changeset, field) do
validate_change(changeset, field, fn _field, value ->
cond do
is_nil(value) -> []
Date.compare(value, Date.utc_today()) == :lt -> []
true -> [{field, "must be in the past"}]
end
end)
end
Custom Validations
params = %{"name" => "Thelonius Monk",
"birth_date" => "2117-10-10"}
changeset =
%Artist{}
|> cast(params, [:name, :birth_date])
|> validate_required(:name)
|> validate_in_the_past(:birth_date)
changeset.errors
Custom Validations
Executing
params = %{name: "Gene Harris"}
changeset =
%Artist{}
|> cast(params, [:name])
|> validate_required([:name])
case Repo.insert(changeset) do
{:ok, artist} -> IO.puts("Record created.")
{:error, changeset} -> IO.inspect(changeset.errors)
end
#=> "Record created."
Executing
params = %{name: nil}
changeset =
%Artist{}
|> cast(params, [:name])
|> validate_required([:name])
case Repo.insert(changeset) do
{:ok, artist} -> IO.puts("Record created.")
{:error, changeset} -> IO.inspect(changeset.errors)
end
#=> [name: {"can't be blank", [validation: :required]}]
Executing
defmodule MusicDB.Artist do
schema "artists" do
...
end
def changeset(artist, params) do
artist
|> cast(params, [:name, :birth_date, :death_date])
|> validate_required([:name])
end
end
Changesets With Schemas
Changesets and Associations
artist = Repo.get_by(Artist, name: "Miles Davis")
new_album = Ecto.build_assoc(artist, :albums)
#=> %MusicDB.Album{artist_id: 1, ...}
Add A New Child Record
artist = Repo.get_by(Artist, name: "Miles Davis")
new_album = Ecto.build_assoc(artist, :albums,
title: "Miles Ahead")
#=> %MusicDB.Album{artist_id: 1,
# title: "Miles Ahead", ...}
Add A New Child Record
Changing All Child Records At Once
Changing All Child Records At Once
put_assoc
cast_assoc
albums = [%Album{title: "Made In Brazil"},
%Album{title: "Dance Of Time"}]
%Artist{name: "Eliane Elias"}
|> change
|> put_assoc(:albums, albums)
|> Repo.insert
Using Internal Data
params = %{name: "Eliane Elias", albums: [
%{title: "Made In Brazil"}, %{title: "Dance Of Time"}
]}
%Artist{}
|> cast(params, [:name])
|> cast_assoc(:albums)
|> Repo.insert
Using External Data
params = %{name: "Eliane Elias", albums: [
%{title: "Made In Brazil"}, %{title: "Dance Of Time"}
]}
%Artist{}
|> cast(params, [:name])
|> cast_assoc(:albums)
|> Repo.insert
# must have "albums" field in params
# must have Album.changeset function
Using External Data
Making Updates With Associations
Repo.get_by(Artist, name: "Miles Davis")
|> change
|> put_assoc(:albums, [%Album{title: "Miles Ahead"}])
|> Repo.update()
Updates With Associations
Repo.get_by(Artist, name: "Miles Davis")
|> change
|> put_assoc(:albums, [%Album{title: "Miles Ahead"}])
|> Repo.update()
#=> ** (RuntimeError) attempting to cast or change
#=> association `albums` from `MusicDB.Artist` that
#=> was not loaded. Please preload your associations
#=> before manipulating them through changesets
Updates With Associations
Repo.get_by(Artist, name: "Miles Davis")
|> Repo.preload(:albums)
|> change
|> put_assoc(:albums, [%Album{title: "Miles Ahead"}])
|> Repo.update()
Updates With Associations
Repo.get_by(Artist, name: "Miles Davis")
|> Repo.preload(:albums)
|> change
|> put_assoc(:albums, [%Album{title: "Miles Ahead"}])
|> Repo.update()
#=> ** (RuntimeError) you are attempting to change
#=> relation :albums of MusicDB.Artist but the
#=> `:on_replace` option of this relation is set
#=> to `:raise`...
Updates With Associations
defmodule MusicDB.Artist do
schema "artists" do
...
has_many :albums, Album
end
...
end
Updates With Associations
defmodule MusicDB.Artist do
schema "artists" do
...
has_many :albums, Album, on_replace: :nilify
end
...
end
Updates With Associations
Repo.get_by(Artist, name: "Miles Davis")
|> Repo.preload(:albums)
|> change
|> put_assoc(:albums, [%Album{title: "Miles Ahead"}])
|> Repo.update()
#=> {:ok, ... }
Updates With Associations
import Ecto.Changeset
# casting/filtering
changeset = change(%Artist{name: "Billie Holiday"})
changeset = cast(%Artist{}, %{name: "Billie Holiday"}, [:name])
# validating
%Artist{}
|> cast(params, [:name, :birth_date])
|> validate_required([:name, :birth_date])
# associations
artist = Repo.get_by(Artist, name: "Miles Davis")
new_album = Ecto.build_assoc(artist, :albums)
%Artist{name: "Eliane Elias"}
|> change
|> put_assoc(:albums, [%Album{title: "Made In Brazil"}])
params = %{name: "Eliane Elias", albums: [title: "Made In Brazil"]
%Artist{}
|> cast(params, [:name])
|> cast_assoc(:albums)
|> Repo.insert
Exercises
Transactions & Multi
album_changeset = Album.changeset(params)
Multiple Operations
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Multiple Operations
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Repo.transaction(fn ->
Repo.update!(album_changeset)
Repo.update!(user_changeset)
end)
Multiple Operations
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Repo.transaction(fn ->
case Repo.update(album_changeset) do
{:error, _changes} ->
IO.puts "Album update failed"
{:ok, _changes} ->
case Repo.update(user_changeset) do
{:error, _changes} ->
IO.puts "User update failed"
{:ok, _changes} ->
IO.puts "Success"
end
end
end)
Multiple Operations
The Multi Module
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Multi.new
Using Multi
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Multi.new
|> Multi.update(:album, album_changeset)
Using Multi
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Multi.new
|> Multi.update(:album, album_changeset)
|> Multi.update(:user, user_changeset)
Using Multi
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
Multi.new
|> Multi.update(:album, album_changeset)
|> Multi.update(:user, user_changeset)
|> Repo.transaction
Using Multi
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
result =
Multi.new
|> Multi.update(:album, album_changeset)
|> Multi.update(:user, user_changeset)
|> Repo.transaction
Using Multi
case result do
{:ok, _changes} ->
IO.puts "Success!"
{:error, :album, _failed_value, _changes_so_far} ->
IO.puts "Album update failed"
{:error, :user, _failed_value, _changes_so_far} ->
IO.puts "User update failed"
end
Using Multi
def check_user_progress(%{user: user}) do
# count user contributions
# award badge if past threshold
# send email if badge earned
end
Run Anything
album_changeset = Album.changeset(params)
user_changeset = User.add_contribution_changeset()
result =
Multi.new
|> Multi.update(:album, album_changeset)
|> Multi.update(:user, user_changeset)
|> Multi.run(:user_progress, &check_user_progress/1)
|> Repo.transaction
Run Anything
Multi.to_list(multi)
Testability
Multi.to_list(multi)
[album: {:update,
#Ecto.Changeset<action: :update,
changes: %{title: "A Love Supreme"},
errors: [], data: #MyApp.Album<>,
valid?: true>, []}
user: {:update,
#Ecto.Changeset<action: :update,
changes: %{edit_count: 151},
errors: [], data: #MyApp.User<>,
valid?: true>, []}
]
Testability
Programming Ecto
by Darin Wilson and Eric Meadows-Jönsson
FROM PRAGMATIC BOOKSHELF
https://pragprog.com
Discount code: ElixirConf_2019_ecto
(through 9/5)
Main Title Example
Subtitle Example
Title with Image
-
List Item Example
-
List Item Example
OPTIONAL SECONDARY HEADER
Example Title No Image
Normal text example. This content should be a max of two to three sentences per slide. Left-aligned so that it's easier to read.
OPTIONAL SECONDARY HEADER
defmodule MusicDB.Album do
use Ecto.Schema
import Ecto.Changeset
alias MusicDB.{Artist, Track, Genre}
schema "albums" do
field(:title, :string)
timestamps()
belongs_to(:artist, Artist)
has_many(:tracks, Track)
many_to_many(:genres, Genre, join_through: "albums_genres")
has_many :notes, MusicDB.Note
end
end
Ecto Workshop - ElixirConf 2019
By Infinite Red
Ecto Workshop - ElixirConf 2019
1-day workshop, presented by Darin Wilson & Eric Meadows-Jönsson
- 1,972