yurii bodarev - otp, phoenix & ecto: three pillars of elixir
TRANSCRIPT
OTP, Phoenix & Ecto: Three Pillars of Elixir
Elixir Club Ternopil, 2017
Erlang
Functional Programming in Erlang
www.futurelearn.com/courses/functional-programming-erlang/
Concurrent Programming in Erlang
https://www.futurelearn.com/courses/concurrent-programming-erlang
Learn You Some Erlang for great good!
http://learnyousomeerlang.com/
Overview
1. Processes & OTP
2. Phoenix Elixir web framework
3. Ecto database wrapper and language integrated query for Elixir
Erlang/Elixir processes
• All code runs inside processes
• Processes are isolated from each other, run concurrent to one another and communicate via message passing (Actor model)
• Processes are extremely lightweight in terms of memory and CPU and managed by Erlang VM
• It is common to have tens or even hundreds of thousands of processes running simultaneously
Spawning basic process
iex> spawn fn -> 100 * 100 end
#PID<0.94.0>
iex> self()
#PID<0.80.0>
Sending and receiving messages
iex> parent = self()
#PID<0.80.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.103.0>
iex> receive do
...> {:hello, pid} -> "Got hello from #{inspect pid}"
...> {:other, _} -> "Something other"
...> end
"Got hello from #PID<0.103.0>"
Receive timeout
iex> receive do
...> {:other, msg} -> msg
...> after
...> 1_000 -> "Nothing received after 1s"
...> end
"Nothing received after 1s"
Flush
iex> send self(), :hello
:hello
iex> send self(), :world
:world
iex> flush()
:hello
:world
:ok
Linked processes: spawn_link
iex> spawn_link fn -> raise "something bad happened" end
23:53:50.503 [error] Process #PID<0.93.0> raised an exception
** (RuntimeError) something bad happened
:erlang.apply/2
** (EXIT from #PID<0.91.0>) an exception was raised:
** (RuntimeError) something bad happened
:erlang.apply/2
Linked processes & “Failing fast” philosophy
Parent process, which is the shell process, has received an EXIT signalfrom another process causing the parent process to terminate.
Often we will link our processes to supervisors which will detect when a process dies and start a new process in its place.
In Elixir we are actually fine with letting processes fail because we expect supervisors to properly restart our systems.
Elixir: Task
iex> task = Task.async(fn -> 100 * 100 end)
%Task{owner: #PID<0.94.0>, pid: #PID<0.96.0>, ref: #Reference<0.0.1.139>}
iex> res = Task.await(task)
10000
Statedef start_link do
Task.start_link(fn -> loop(%{}) end)
end
defp loop(map) do
receive do
{:get, key, caller} ->
send caller, Map.get(map, key)
loop(map)
{:put, key, value} ->
loop(Map.put(map, key, value))
end
end
State
Spawn LoopInit Exit
SendReceive
Elixir: Agent
iex> {:ok, agent} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.88.0>}
iex> Agent.update(agent, &Map.put(&1, :hello, "world"))
:ok
iex> Agent.get(agent, &Map.get(&1, :hello))
"world"
Behaviours
• Many of the processes have similar structures, they follow similar patterns
• Behaviours provide a way to define a set of functions that have to be implemented by a module
• You can think of behaviours like interfaces in OO languages
Behaviours
defmodule GenServer do
@callback init(args :: term) :: {:ok, state} ...
@callback handle_call(request :: term, from, state :: term)
:: {:reply, reply, new_state} ...
@callback handle_cast(request :: term, state :: term)
:: {:noreply, new_state} ...
...
Implementing behaviours
...# Callbacks
def handle_call(:pop, _from, [h | t]) do{:reply, h, t}
end
def handle_cast({:push, item}, state) do{:noreply, [item | state]}
end...
GenServer
• “Generic servers” (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
• The GenServer behaviour abstracts the common client-server interaction. Developers are only required to implement the callbacks and functionality they are interested in.
• A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously and so on.
GenServer exampledefmodule Stack do
use GenServer
# Callbacks
def handle_call(:pop, _from, [h | t]) do
{:reply, h, t}
end
def handle_cast({:push, item}, state) do
{:noreply, [item | state]}
end
end
GenServer example
iex> {:ok, pid} = GenServer.start_link(Stack, [:hello])
iex> GenServer.call(pid, :pop)
:hello
iex> GenServer.cast(pid, {:push, :world})
:ok
iex>GenServer.call(pid, :pop)
:world
GenServer cheatsheet
Benjamin Tan Wei Hao
https://github.com/benjamintanweihao/elixir-cheatsheets
Supervision Trees
• Supervision trees are a nice way to structure fault-tolerant applications.
• Process structuring model based on the idea of workers and supervisors.
S
W
S
W
S
W
Supervision Trees
• Workers are processes that perform computations, that is, they do the actual work.
• Supervisors are processes that monitor the behaviour of workers. A supervisor can restart a worker if something goes wrong.
• The supervision strategy dictates what happens when one of the children crashes.
Supervisor
• A behaviour module for implementing supervision functionality.
• A supervisor is a process which supervises other processes, which we refer to as child processes.
Supervisor module
defmodule MyApp.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, [])
end
def init([]) do
children = [ worker(Stack, [[:hello]]) ]
supervise(children, strategy: :one_for_one)
end
end
Supervisor Cheat Sheet
Benjamin Tan Wei Hao
https://github.com/benjamintanweihao/elixir-cheatsheets
Application behaviour
• In Erlang/OTP, an application is a component implementing some specific functionality, that can be started and stopped as a unit
• Mix is responsible for compiling your source code and generating your application .app file in Elixir.
• Mix is also responsible for configuring, starting and stopping your application and its dependencies (mix.exs).
• .app holds our application definition
Application callback
defmodule MyApp do
use Application
def start(_type, _args) do
MyApp.Supervisor.start_link()
end
end
Application project
mix new hello_world --sup hello_world
|-- README.md
|-- config
| `-- config.exs
|-- lib
| |-- hello_world
| | `-- application.ex
| `-- hello_world.ex
|-- mix.exs
`-- test
|-- hello_world_test.exs
`-- test_helper.exs
Application project: configuration
hello_world\mix.exs
def application do
# Specify extra applications you'll use from Erlang/Elixir[extra_applications: [:logger],
mod: {HelloWorld.Application, []}]
end
Application project: callback modulehello_world\lib\hello_world\application.ex
defmodule HelloWorld.Application do
...
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = []
opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
Supervisor.start_link(children, opts)
end
end
Umbrella projects
mix new hello_umbrella --umbrella
hello_umbrella
|-- README.md
|-- apps
|-- config
| `-- config.exs
`-- mix.exs
Umbrella projects: configuration
...
def project do
[apps_path: "apps",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps()]
end
...
In umbrella dependencies
Mix supports an easy mechanism to make one umbrella child depend on another.
...
defp deps do
[{:hello_world, in_umbrella: true}]
end
...
GenStage & Flow
Announcing GenStage
http://elixir-lang.org/blog/2016/07/14/announcing-genstage/
GenStage and Flow - Jose Valim | Elixir Club 5
https://www.youtube.com/watch?v=IUrfcBwkm7w
https://hex.pm/packages/gen_stage
GenStage is a new Elixir behaviour for exchanging events with back-pressure between Elixir processes
producerproducer consumer
producer consumer
consumer
Flow: concurrent data processingdef process_flow(path_to_file) do
path_to_file
|> File.stream!()
|> Flow.from_enumerable()
|> Flow.flat_map(&String.split/1)
|> Flow.map(&String.replace(&1, ~r/\W/u, ""))
|> Flow.filter_map(fn w -> w != "" end, &String.downcase/1)
|> Flow.partition()
|> Flow.reduce(fn -> %{} end, fn word, map ->
Map.update(map, word, 1, &(&1 + 1))
end)
|> Enum.into(%{})
end
Flow: concurrent data processingdef process_flow(path_to_file) do
path_to_file
|> File.stream!()
|> Flow.from_enumerable()
|> Flow.flat_map(&String.split/1)
|> Flow.map(&String.replace(&1, ~r/\W/u, ""))
|> Flow.filter_map(fn w -> w != "" end, &String.downcase/1)
|> Flow.partition()
|> Flow.reduce(fn -> %{} end, fn word, map ->
Map.update(map, word, 1, &(&1 + 1))
end)
|> Enum.into(%{})
end
P
PC PC
DemandDispatcher
PartitionDispatcher
PC PC
C CReducers%{} %{}
FlowP
PC PC
PC PC
C C%{} %{}
"The Project Gutenberg EBook of The Complete Works of William Shakespeare, by\n"
"William Shakespeare\n"
"The", "Project", "Gutenberg", "EBook", "of", "The", "Complete", "Works", "of", "William", "Shakespeare,", "by"
"William", "Shakespeare"
"the", "project", "of", “the", "william", "of", "by ", "william"
"gutenberg", "ebook", "complete", "shakespeare", "works", "shakespeare"
Flow
Experimental.Flow, Yurii Bodarev at KyivElixirMeetup 3.1
https://www.youtube.com/watch?v=XhUeSUFF06w
https://github.com/yuriibodarev/elixir_flow
Phoenix Framework
• Phoenix is a web development framework written in Elixir which implements the server-side MVC pattern
• Phoenix provides the best of both worlds - high developer productivity and high application performance
• Phoenix is actually the top layer of a multi-layer system designed to be modular and flexible. The other layers include Plug, and Ecto
• The Erlang HTTP server, Cowboy, acts as the foundation for Plug and Phoenix
Phoenix Framework
• The Plug
• The Endpoint
• The Router
• Controllers
• Actions
• Views
• Templates
• Channels
The Plug
• Plug is a specification for constructing composable modules to build web applications.
• Plugs are reusable modules or functions built to that specification.
• They provide discrete behaviors - like request header parsing or logging.
• Because the Plug API is small and consistent, plugs can be defined and executed in a set order, like a pipeline.
• Core Phoenix components like Endpoints, Routers, and Controllers are all just Plugs internally
Module Plug example
defmodule Example.HelloWorldPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _opts) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello World!")
end
end
%Plug.Conn{…}
Plug pipelines
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
Module Plug example
...
@locales ["en", "fr", "de"]
def init(default), do: default
def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default)
when loc in @locales
do
assign(conn, :locale, loc)
end
def call(conn, default), do: assign(conn, :locale, default)
...
Adding Plug to the pipeline
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug PhoenixApp.Plugs.Locale, "en"
end
The Endpoint
• provides a wrapper for starting and stopping the endpoint as part of a supervision tree;
• handles all aspects of requests up until the point where the router takes over
• to define an initial plug pipeline where requests are sent through;
• to host web specific configuration for your application.
• dispatches requests into a designated router
The Endpointphoenix_app\lib\phoenix_app\web\endpoint.ex
defmodule PhoenixApp.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix_app
# plug ...
# plug ...
plug PhoenixApp.Web.Router
end
Starting Endpointphoenix_app\lib\phoenix_app\application.ex
...
children = [
# Start the Ecto repository
supervisor(PhoenixApp.Repo, []),
# Start the endpoint when the application startssupervisor(PhoenixApp.Web.Endpoint, [])
]
opts = [strategy: :one_for_one, name: PhoenixApp.Supervisor] Supervisor.start_link(children, opts)
...
The Router
• parses incoming requests and dispatches them to the correct controller/action, passing parameters as needed
• provides helpers to generate route paths or urls to resources
• defines named pipelines through which we may pass our requests
The Routerphoenix_app\lib\phoenix_app\web\router.ex
...
pipeline :browser do
plug :accepts, ["html"]
...
plug :put_secure_browser_headers
end
scope "/", PhoenixApp.Web do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
...
Controllers & Actions
Controllers provide functions, called actions, to handle requests
Actions
• prepare data and pass it into views
• invoke rendering via views
• perform redirects
Controllerget "/pages/:id", PageController, :show
phoenix_app\lib\phoenix_app\web\controllers\page_controller.ex
defmodule PhoenixApp.Web.PageController do
use PhoenixApp.Web, :controller
def show(conn, %{"id" => id}) do
user = Accounts.get_user(id)
render(conn, "show.html", user: user)
end
end
web\web.exuse PhoenixApp.Web, :controller
phoenix_app\lib\phoenix_app\web\web.ex...
def controller do
quote do
use Phoenix.Controller, namespace: PhoenixApp.Web
import Plug.Conn
import PhoenixApp.Web.Router.Helpers
import PhoenixApp.Web.Gettext
end
end
...
Views
• Defines the view layer of a Phoenix application
• Render templates
• Define helper functions, available in templates, to decorate data for presentation
Rendering Templates
Phoenix assumes a strong naming convention from controllers to views to the templates they render. The PageController requires a PageViewto render templates in the \web\templates\page directory.
phoenix_app\lib\phoenix_app\web\web.ex
...
def view do quote do
use Phoenix.View, root: "lib/phoenix_app/web/templates", namespace: PhoenixApp.Web
...
Rendering Templates
phoenix_app\lib\phoenix_app\web\views\page_view.ex
defmodule PhoenixApp.Web.PageView do
use PhoenixApp.Web, :view
end
Phoenix.View will automatically load all templates at “phoenix_app\lib\phoenix_app\web\templates\page” and include them in the PhoenixApp.Web.PageView
Rendering JSON
def render("index.json", %{pages: pages}) do
%{data: render_many(pages, PhoenixApp.PageView, "page.json")}
end
def render("show.json", %{page: page}) do
%{data: render_one(page, PhoenixApp.PageView, "page.json")}
end
def render("page.json", %{page: page}) do
%{title: page.title}
end
Templates
foo.html.eex
• templates are precompiled and fast
• template name - is the name of the template as given by the user, without the template engine extension, for example: “foo.html”
• template path - is the complete path of the template in the filesystem, for example, “path/to/foo.html.eex”
• template root - the directory where templates are defined
• template engine (EEx)- a module that receives a template path and transforms its source code into Elixir quoted expressions.
Template examples
Hello <%= @name %>
<h3>Keys for the conn Struct</h3>
<%= for key <- connection_keys @conn do %>
<p><%= key %></p>
<% end %>
function that returns List of keys
Channels
• manage sockets for easy real-time communication
• are analogous to controllers except that they allow bi-directional communication with persistent connections
• Every time you join a channel, you need to choose which particular topic you want to listen to. The topic is just an identifier, but by convention it is often made of two parts: "topic:subtopic".
Channel endpoint
phoenix_app\lib\phoenix_app\web\endpoint.ex
socket "/socket", PhoenixApp.Web.UserSocket
phoenix_app\lib\phoenix_app\web\channels\user_socket.ex
channel "room:*", PhoenixApp.Web.RoomChannel
Any topic coming into the router with the "room:" prefix would dispatch to MyApp.RoomChannel
Joining Channels
defmodule PhoenixApp.Web.RoomChannel douse Phoenix.Channel
def join("room:lobby", _message, socket) do{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do{:error, %{reason: "unauthorized"}} end
end
JS
phoenix_app\assets\js\socket.js...
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {}) channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
JS
…
channel.push("new_msg", {body: “Hello world!”})
…
…
channel.on("new_msg", payload => { … }) …
Incoming Events
We handle incoming events with handle_in/3. We can pattern match on the event names, like “new_msg”
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast! socket, "new_msg", %{body: body}
{:noreply, socket} end
Outgoing Events: default implementation
def handle_out("new_msg", payload, socket) do
push socket, "new_msg", payload
{:noreply, socket} end
Intercepting Outgoing Events
intercept ["smth_important"]
def handle_out("smth_important", msg, socket) do
if … do
{:noreply, socket}
else
push socket, " smth_important", msg
{:noreply, socket}
endend
Phoenix Framework 1.3
v1.3.0-rc.1
https://hex.pm/packages/phoenix
phx.new project generator
mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
Phoenix Framework 1.3
Lonestar ElixirConf 2017- KEYNOTE: Phoenix 1.3 by Chris McCord
https://www.youtube.com/watch?v=tMO28ar0lW8
Phoenix v1.3.0-rc.0 released @ ElixirForum
https://elixirforum.com/t/phoenix-v1-3-0-rc-0-released/3947
Upcoming book “Programming Phoenix 1.3” @ ElixirForum
https://elixirforum.com/t/programming-phoenix-1-3/2469
1.2: mix phoenix.new phoenix_oldphoenix_old
|-- config
|-- lib
| `-- phoenix_old
|-- priv
|-- test
`-- web
|-- channels
|-- controllers
|-- models
|-- static
|-- templates
`-- views
1.3: mix phx.new phoenix_new
phoenix_new
|-- assets
|-- config
|-- lib
| `-- phoenix_new
| `-- web
| |-- channels
| |-- controllers
| |-- templates
| `-- views
|-- priv
`-- test
1.3: mix phx.new phoenix --umbrella
phoenix_umbrella
|-- apps
| |-- phoenix
…
| `-- phoenix_web
phoenix_umbrella\apps\phoenix_web\mix.exs
...
{:phoenix, in_umbrella: true},
...
Web Namespace
defmodule PhoenixApp.Web.PageController do
use PhoenixApp.Web, :controller
def index(conn, _params) do
render conn, "index.html"
endend
Context
1.2: mix phoenix.gen.json User users email:string
Repo.get(User, id)
1.3: mix phx.gen.json Accounts User users email:string
Accounts.get_user(2) iex> %User{email: [email protected]}
Accounts.create_user(params) iex> {:ok, new_user}
1.2: Models
`-- web
|-- channels
|-- controllers
|-- models
| |-- comment.ex
| |-- invoice.ex
| |-- order.ex
| |-- payment.ex
| |-- post.ex
| `-- user.ex
|-- static
|-- templates
`-- views
1.3: Context
|-- lib
| `-- phoenix_new
| |-- blog
| | |-- blog.ex
| | |-- comment.ex
| | `-- post.ex
| |-- sales
| | |-- order.ex
| | |-- payment.ex
| | `-- sales.ex
| `-- web
| |-- channels
The boundary for the Sales system.lib\phoenix_new\sales\sales.ex
defmodule PhoenixNew.Sales do
def list_orders do
Repo.all(Order)
end
def get_order!(id), do: Repo.get!(Order, id)
def create_order(attrs \\ %{}) do
%Order{} |> order_changeset(attrs) |> Repo.insert()
end
...
Action Fallback
On Phoenix 1.2, every controller needed to return a valid %Plug.Conn{} for every request. Otherwise, an exception would rise.
On Phoenix 1.3 we can register the plug to call as a fallback to the controller action. If the controller action fails to return a %Plug.Conn{}, the provided plug will be called and receive the controller’s %Plug.Conn{} as it was before the action was invoked along with the value returned from the controller action.
1.2: Controller Actionphoenix_old\web\controllers\post_controller.excase Repo.insert(changeset) do
{:ok, post} ->
conn
|> put_status(:created)
|> put_resp_header("location", post_path(conn, :show, post))
|> render("show.json", post: post)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(PhoenixOld.ChangesetView, "error.json", changeset:changeset)
end
1.3: Controller Action
phoenix_new\lib\phoenix_new\web\controllers\post_controller.ex
def create(conn, %{"post" => post_params}) do
with {:ok, %Post{} = post} <- Blog.create_post(post_params) do
conn
|> put_status(:created)
|> put_resp_header("location", post_path(conn, :show, post))
|> render("show.json", post: post)
end
end
1.3 Action Fallback
phoenix_new\lib\phoenix_new\web\controllers\fallback_controller.ex
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> render(PhoenixNew.Web.ChangesetView, "error.json", changeset:changeset)
end
Ecto
Domain specific language for writing queries and interacting with databases in Elixir.
pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0
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.
Repositories
Via the repository, we can create, update, destroy and query existing database entries.
Repositories
Ecto.Repo is a wrapper around the database. We can define a repository as follows:
defmodule Blog.Repo do
use Ecto.Repo, otp_app: :blog
end
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"
RepositoriesEach repository in Ecto defines a start_link/0. Usually this function is invoked as part of your application supervision tree:
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
Schema
Schemas allows developers to define the shape of their data.
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
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}
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]}
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,…}}
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
Changesets
We can add changesets to our schemas to validate changes before we apply them to the data:
def changeset(user, params \\ %{}) do
user
|> cast(params, [:name, :reputation])
|> validate_required([:name, :reputation])
|> validate_inclusion(:reputation, -999..999)
end
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>
Changeset with Repository functions
iex> valid_changeset.valid?
true
iex> Blog.Repo.insert(valid_changeset)
{:ok, %Blog.User{…, id: 7, …}}
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>}
Changeset with Repository functions
case Blog.Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
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
Query
Ecto allows you to write queries in Elixir and send them to the repository, which translates them to the underlying database.
Query using predefined Schema
# Query using predefined Schemaquery = from u in User,
where: u.reputation > 35, select: u
# Returns %User{} structs matching the queryRepo.all(query)
[%Blog.User{…, id: 2, …, name: "Bender", …, reputation: 42, …},
%Blog.User{…, id: 1, …, name: "Alex", …, reputation: 144, …}]
Direct query with “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}]
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
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
Ecto Multi
Ecto.Multi is a data structure for grouping multiple Repo operations in a single database transaction.
def reset(account, params) do
Multi.new
|> Multi.update(:account, Account.password_reset_changeset(account, params))
|> Multi.insert(:log, Log.password_reset_changeset(account, params))
|> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions))
end
Repo.transaction(PasswordManager.reset(account, params))
Ecto Multi
case result do
{:ok, %{account: account, log: log, sessions: sessions}} ->
# We can access results under keys we used
# for naming the operations.
{:error, failed_operation, failed_value, changes_so_far} ->
# One of the operations failed.
# We can access the operation's failure value (changeset)
# Successful operations would have been rolled back.
end
BooksSaša Jurić “Elixir in Action”
https://www.manning.com/books/elixir-in-action
Benjamin Tan Wei Hao “The Little Elixir & OTP Guidebook”
https://www.manning.com/books/the-little-elixir-and-otp-guidebook
New! Lance Halvorsen “Functional Web Development with Elixir, OTP, and Phoenix”
https://pragprog.com/book/lhelph/functional-web-development-with-elixir-otp-and-phoenix
Chris McCord “Programming Phoenix” (1.2 -> 1.3)
https://pragprog.com/book/phoenix/programming-phoenix
“What's new in Ecto 2.0”
http://pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0
THANK YOU!Yurii Bodarev
@bodarev_yurii
https://github.com/yuriibodarev