85
Leveling Up With Ecto Want to try some of this code? Clone this repo to set up a playground: http://bit.ly/leveling_up ElixirConf 2016

Want to try some of this code? Clone this repo to set up a

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Leveling Up With Ecto

Want to try some of this code? Clone this repo to set up a playground:

http://bit.ly/leveling_up

ElixirConf 2016

SODD

SODD(Stack Overflow Driven Development)

What is Ecto?

Ecto Is...

a toolset for interacting with external datasources

The Big Four

The Big Four

→ Repo→ Query→ Schema→ Changeset

Repo

Repo

The part that actually talks to the datasource

Repo

get()

insert()

update()

delete()

transaction()

Typical use

defmodule MyApp.Repo

use Ecto.Repo, otp_app: :my_app

end

Typical use

defmodule MyApp.MySuperAwesomeDb!!!♥#

use Ecto.Repo, otp_app: :my_app

end

Typical use

defmodule MyApp.Repo

use Ecto.Repo, otp_app: :my_app

end

Typical use

defmodule MyApp.Repo

use Ecto.Repo, otp_app: :my_app

end

config :my_app, MyApp.Repo, adapter: Ecto.Adapters.Postgres, database: "my_app_db", username: "postgres", password: "postgres", hostname: "localhost"

Typical use

defmodule MyApp.Repo use Ecto.Repo, otp_app: :my_append

...alias MyApp.Repo

Repo.insert_all("artists", [[name: "John Coltrane"]])

Connect to another data source

defmodule MyApp.LegacyRepo

use Ecto.Repo, otp_app: :my_app

end

config :my_app, MyApp.LegacyRepo, adapter: Ecto.Adapters.AncientDB, # <- not actually supported! database: "omg_scary_legacy_db", username: "dba1", password: "luvMatchbox20", hostname: "sparc_station7"

Add your own Repo functions

Add your own Repo functions

defmodule MusicDb.Repo do use Ecto.Repo, otp_app: :music_db

def count(table) do aggregate(table, :count, :id) endend...Repo.count("artists")=> 75

Work directly with the data

The *all functions:insert_all

update_all

delete_all

all

Repo.insert_all("artists", [[name: "John Coltrane"]])=> {1, nil}

Repo.update_all("artists", set: [updated_at: Ecto.DateTime.utc])=> {75, nil}

Repo.delete_all("artists")=> {75, nil}

But what about queries?

Low-level queries

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

Query

Feels a lot like SQL (in a good way)

SELECT t.id, t.title, a.title FROM tracks t JOIN albums a ON t.album_id = a.id WHERE t.duration > 600;

query = from t in "tracks", join: a in "albums", on: t.album_id == a.id, where: t.duration > 600, select: [t.id, t.title, a.title]

Feels a lot like SQL (in a good way)

SELECT t.id, t.title, a.title FROM tracks t JOIN albums a ON t.album_id = a.id WHERE t.duration > 600;

query = from t in "tracks", join: a in "albums", on: t.album_id == a.id, where: t.duration > 600, select: [t.id, t.title, a.title]

Repo.all(query)

Use ^ to interpolate

min_duration = 600

query = from t in "tracks", join: a in "albums", on: t.album_id == a.id,➡ where: t.duration > ^min_duration, select: [t.id, t.title, a.title]

Might have to think about types

min_duration = "600"

query = from t in "tracks", join: a in "albums", on: t.album_id == a.id,➡ where: t.duration > type(^min_duration, :integer), select: [t.id, t.title, a.title]

Queries are composable

(you might call them "scopes")

Queries are composable

albums_by_miles = from a in "albums", join: ar in "artists", on: a.artist_id == ar.id, where: ar.name == "Miles Davis"

Queries are composable

albums_by_miles = from a in "albums", join: ar in "artists", on: a.artist_id == ar.id, where: ar.name == "Miles Davis"

query = from a in albums_by_miles, select: a.title

Repo.all(q)# => ["Cookin' At The Plugged Nickel", "Kind Of Blue"]

Queries are composable

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 in albums_by_miles, join: t in "tracks", on: a.id == t.album_id, select: t.title

Repo.all(q)# => ["If I Were A Bell", "Stella By Starlight" ...]

Use queries in Repo.*all functions

from a in "artists", where: a.name == "Art Taytum"|> Repo.update_all(set: [name: "Art Tatum"])

from t in "tracks", where: t.duration > 600|> Repo.delete_all

The story so far...

We can do free-form queries with Repo.*all, but:→ we have to be explicit with select→ we have to be careful about types

Schema

Schemas define a reusable shape that we can use to move data in

and out of our data source.

defmodule MusicDb.Track do use Ecto.Schema

schema "tracks" do field :title, :string field :duration, :integer field :index, :integer timestamps endend

# without schemaquery = from t in "tracks", where: t.duration > type(^min_duration, :integer), select: [t.id, t.title, t.duration, t.index]

# with schemaquery = from t in Track, where: t.duration > ^min_duration

But you don't always have to use them

# fetch artist name and number of albumsq = from a in Artist, join: al in Album, on: a.id == al.artist_id, group_by: a.id, select: %{name: a.name, album_count: count(al.id)}

Repo.all(q)# => [%{album_count: 2, name: "Miles Davis"}...]

Schemas are a huge help with associations

defmodule MusicDb.Artist do use Ecto.Schema

schema "artists" do➡ has_many :albums, MusicDb.Album

field :name, :string timestamps endend

Associations

has_many

has_one

belongs_to

many_to_many

defmodule MusicDb.Album do use Ecto.Schema

schema "albums" do ➡ belongs_to :artist, musicdb.artist has_many :tracks, musicdb.track many_to_many :genres, musicdb.genre, join_through: "albums_genres"

field :title, :string timestamps endend

defmodule MusicDb.Album do use Ecto.Schema

schema "albums" do belongs_to :artist, MusicDb.Artist ➡ has_many :tracks, MusicDb.Track many_to_many :genres, MusicDb.Genre, join_through: "albums_genres"

field :title, :string timestamps endend

defmodule MusicDb.Album do use Ecto.Schema

schema "albums" do belongs_to :artist, MusicDb.Artist has_many :tracks, MusicDb.Track ➡ many_to_many :genres, MusicDb.Genre, join_through: "albums_genres"

field :title, :string timestamps endend

Seeing the association records

album = Repo.get(Album, 1)

album.tracks

The dreaded#Ecto.Association.NotLoaded

The dreaded#Ecto.Association.NotLoaded

But why?

Ecto won't fetch associated recordsunless you ask it to

(and this is a good thing)

How to fix it

Use preload in your query# fetch Bobby Hutcherson and all his albumsquery = from a in Artist, where: a.name == "Bobby Hutcherson",➡ preload: [:albums]

How to fix it

(you can nest these)# fetch Bobby Hutcherson and all his albums with all the tracksquery = from a in Artist, where: a.name == "Bobby Hutcherson",➡ preload: [albums: :tracks]

How to fix it

Use Repo.preload after the fact# fetch artist with id 1 and their albumsArtist |> Repo.get(1) |> Repo.preload(:albums)

Inserting new records is a breeze

Repo.insert! %Artist{ name: "Bobby Hutcherson", albums: [ %Album{ title: "Live At Montreaux", tracks: [ %Track{ title: "Anton's Ball", index: 1 }, %Track{ title: "The Moontrane", index: 2 }, %Track{ title: "Farallone", index: 3 }, %Track{ title: "Song Of Songs", index: 4 } ] } ]}

...as long as your data is valid.

Changeset

Changesets allow you to filter, cast, and validate values that you want to send to

your datasource

Filter and cast

import Ecto.Changeset

# %{"title" => "So What", "index" => "1"}params = get_user_input()

changeset = %Track{} |> cast(params, [:title, :index])

Validate

import Ecto.Changeset

# %{"title" => "So What", "index" => "1"}params = get_user_input()

changeset = %Track{} |> cast(params, [:title, :index]) |> validate_required([:title, :index]) |> validate_number(:index, greater_than: 0)

See if it works

case Repo.insert(changeset) do {:ok, track} -> IO.puts "Track #{track.name} succesfully added" {:error, changeset} -> IO.puts changeset.errorsend

Validations

validate_acceptance validate_change validate_confirmation validate_exclusion validate_format validate_inclusion validate_length validate_number validate_required validate_subset

Constraints

assoc_constraint check_constraint exclusion_constraint foreign_key_constraint no_assoc_constraint unique_constraint

Schemaless changesets

Schemaless changesets

types = %{band_name: :string, album_name: :string}params = %{band_name: "Modern Jazz Quartet", album_name: ""}changeset = {%{}, types} |> cast(params, Map.keys(types)) |> validate_required([:band_name, :album_name])

changeset.valid?# => falsechangeset.errors# => [album_name: {"can't be blank", []}]

Phoenix integration

phoenix_ecto

Phoenix integration

= form_for @changeset, @action, fn f ->

.field = label f, :title = text_input f, :title = error_tag f, :title

.field = label f, :index = text_input f, :index = error_tag f, :index...

defmodule MusicDb.Track do use Ecto.Schema

schema "tracks" do belongs_to :album, MusicDb.Album

field :title, :string field :duration, :integer field :index, :integer timestamps endend

defmodule MusicDb.Track do use Ecto.Schema

schema "tracks" do belongs_to :album, MusicDb.Album

field :title, :string field :duration, :integer field :index, :integer ➡ field :duration_string, :string, virtual: true timestamps endend

= form_for @changeset, @action, fn f ->

...

.field = label f, :duration_string = text_input f, :duration_string = error_tag f, :duration_string

...

Validate, then convert

changeset = %Track{} |> cast(params, [:title, :index, :duration_string]) |> validate_required([:title, :index]) |> validate_number(:index, greater_than: 0)

Validate, then convert

changeset = %Track{} |> cast(params, [:title, :index, :duration_string]) |> validate_required([:title, :index]) |> validate_number(:index, greater_than: 0) |> convert_duration_string_to_integer()

Validate, then convert

def convert_duration_string_to_integer(changeset) do duration_string = get_field(changeset, :duration_string) duration_integer = convert_to_integer(duration_string) put_change(changeset, :duration, duration_integer)end

Validate, then convert

import Ecto.Changeset

params = get_user_input()allowed_fields = [:title, :index, :duration_string]changeset = %Track{} |> cast(params, allowed_fields) |> validate_required([:title, :index]) |> validate_number(:index, greater_than: 0) |> convert_duration_string_to_integer() |> validate_number(:duration, greater_than: 0)

Schemas don't have to have a database table

embedded_schema do

...

end

Schemas don't have to have a database table

Ecto's insert_all and schemaless queries

http://blog.plataformatec.com.br/2016/05/ectos-insert_all-and-schemaless-queries/

But what does it all mean?

Flexibility

More leveling up

Github: https://github.com/elixir-ecto/ectoDocumentation: https://hexdocs.pm/ecto/Ecto.html

Thanks

@darinwilson

Photos (from Flickr): abbybatchelder, looka, eleaf,

85552598@N03