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