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
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
defmodule MusicDb.Repo do use Ecto.Repo, otp_app: :music_db
def count(table) do aggregate(table, :count, :id) endend...Repo.count("artists")=> 75
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}
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}}]]}}
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
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
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"}...]
defmodule MusicDb.Artist do use Ecto.Schema
schema "artists" do➡ has_many :albums, MusicDb.Album
field :name, :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
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
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 } ] } ]}
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
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
= 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
Ecto's insert_all and schemaless queries
http://blog.plataformatec.com.br/2016/05/ectos-insert_all-and-schemaless-queries/
More leveling up
Github: https://github.com/elixir-ecto/ectoDocumentation: https://hexdocs.pm/ecto/Ecto.html