Ecto and Phoenix: Doing web with Elixir - Yurii Bodarev

67 %
33 %
Information about Ecto and Phoenix: Doing web with Elixir - Yurii Bodarev

Published on December 28, 2017

Author: Elixir-Meetup


1. Ecto and Phoenix: Doing Web With Elixir Elixir Club 9, Kharkiv 2017

2. • Elixir Applications • Umbrella Projects • Phoenix: Application Structure • Phoenix: Controllers, Templates and Views • Phoenix: JSON • Phoenix: Channels • ECTO

3. Elixir (Erlang.OTP) Applications • In Elixir (Erlang/OTP), an application is a component implementing some specific functionality, that can be started and stopped as a unit, and which can be re-used in other systems. • Applications are defined with an application file named in the same ebin directory as the compiled modules of the application. • In Elixir, the Mix build tool is responsible for compiling your source code and generating your application .app file. 

4. Creating Elixir Application with Supervision Tree $ mix new hello_world --sup $ mix compile

5. _build/dev/lib/hello_world/ebin/ (Erlang/OTP) {application,hello_world, [{applications,[kernel,stdlib,elixir,logger]}, {description,"hello_world"}, {modules, ['Elixir.HelloWorld','Elixir.HelloWorld.Application']}, {registered,[]}, {vsn,"0.1.0"}, {extra_applications,[logger]}, {mod,{'Elixir.HelloWorld.Application',[]}}]}.

6. mix.exs def project do [ app: :hello_world, version: "0.1.0", elixir: "~> 1.6-dev", start_permanent: Mix.env() == :prod, deps: deps() ] end def application do [ extra_applications: [:logger], mod: {HelloWorld.Application, []} ] end defp deps do [] end

7. Starting Elixir Application • You start one or more applications, each with their own initialization and termination logic. • Elixir does not have a main procedure that is responsible for starting your system. • Starting an application is done via the “application module callback”, which is a module that defines the start/2 function.

8. lib/hello_world/application.ex use Application def start(_type, _args) do # List all child processes to be supervised children = [ # Starts a worker by calling: HelloWorld.Worker.start_link(arg) # {HelloWorld.Worker, arg}, ] opts = [strategy: :one_for_one, name: HelloWorld.Supervisor] Supervisor.start_link(children, opts) end

9. Application Supervision Tree The start/2 function should start a supervisor, which is often called as the top-level supervisor, since it sits at the root of a potentially long supervision tree. lib/hello_phoenix/application.ex def start(_type, _args) do import Supervisor.Spec children = [ # Start the Ecto repository supervisor(HelloPhoenix.Repo, []), # Start the endpoint when the application starts supervisor(HelloPhoenixWeb.Endpoint, []), ] opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor] Supervisor.start_link(children, opts) end WW S S S W

10. Umbrella projects $ mix new hello_umbrella --umbrella

11. Umbrella projects hello_umbrella/mix.exs def project do [ apps_path: "apps", start_permanent: Mix.env() == :prod, deps: deps() ] end

12. Applications in umbrella project $ cd hello_umbrella/apps $ mix new hello $ mix new world

13. In umbrella dependencies hello_umbrella/apps/hello/mix.exs def project do [ app: :hello, version: "0.1.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.6-dev", start_permanent: Mix.env() == :prod, deps: deps() ] end ... defp deps do [ {:world, in_umbrella: true} ] end

14. Phoenix Framework: Productive. Reliable. Fast. • Phoenix is a web development framework written in Elixir which implements the server- side MVC pattern.  • Current version: 1.3.0 •

15.  Phoenix Layers • Phoenix itself is actually the top layer of a multi-layer system • Cowboy: the web server used by Phoenix (and Plug) is Cowboy (Erlang). • Plug is a specification for constructing composable modules to build web applications. Plugs are reusable modules or functions built to that specification. • Ecto is a language integrated query composition tool and database wrapper for Elixir.

16. Phoenix Application $ mix hello_phoenix lib/hello_phoenix/application.ex defmodule HelloPhoenix.Application do use Application ... lib/hello_phoenix_web/endpoint.ex defmodule HelloPhoenixWeb.Endpoint do use Phoenix.Endpoint, otp_app: :hello_phoenix ...

17. Phoenix Application mix.exs ... def application do [ mod: {HelloPhoenix.Application, []}, extra_applications: [:logger, :runtime_tools] ] end ... lib/hello_phoenix/application.ex ... def start(_type, _args) do import Supervisor.Spec children = [ # Start the Ecto repository supervisor(HelloPhoenix.Repo, []), # Start the endpoint when the application starts supervisor(HelloPhoenixWeb.Endpoint, []), ] opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor] Supervisor.start_link(children, opts) end ...

18. Starting Phoenix Application $ mix hello_phoenix $ cd hello_phoenix $ mix deps.get $ mix ecto.create $ mix ecto.migrate $ mix phx.server ($ iex -S mix phx.server)

19. Phoenix Umbrella Project mix phoenix --umbrella

20. Plug • Core Phoenix components like Endpoints, Routers, and Controllers are all just Plugs internally. • Plug is a specification for composable modules in web application. • 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.

21. Plugs in Phoenix’s HTTP layer HTTP Request %Plug.Conn Plug Plug Plug Plug Plug Plug %Plug.Conn HTTP Response

22. Plug.Conn Request # → "" conn.method # → "GET" conn.path_info # → ["posts", "1"] conn.request_path # → "/posts/1" conn.query_string # → "utm_source=twitter" conn.port # → 80 conn.scheme # → :http conn.peer # → { {127, 0, 0, 1}, 12345 } conn.remote_ip # → { 151, 236, 219, 228 } conn.req_headers # → [{"content-type", "text/plain"}] Response conn.resp_body # → "..." conn.resp_charset # → "utf-8" conn.resp_cookies # → ... conn.resp_headers # → ... conn.status # → … Assigns conn |> assign(:user_id, 100) conn.assigns[:user_id]

23. 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{…}

24. Plug pipelines … pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end …

25. Phoenix Parts • Endpoint • Router • Controllers • Views • Templates • Channels

26. 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 • Defines an initial plug pipeline where requests are sent through • Hosts web specific configuration for your application • Dispatches requests into a designated router

27. Endpoint … use Phoenix.Endpoint, otp_app: :hello_phoenix socket "/socket", HelloPhoenixWeb.UserSocket plug Plug.Static, ... if code_reloading? do ... end plug Plug.RequestId plug Plug.Logger plug Plug.Parsers, ... plug Plug.MethodOverride plug Plug.Head plug Plug.Session, ... plug HelloPhoenixWeb.Router …

28. Starting Endpoint ... def start(_type, _args) do import Supervisor.Spec children = [ # Start the Ecto repository supervisor(HelloPhoenix.Repo, []), # Start the endpoint when the application starts supervisor(HelloPhoenixWeb.Endpoint, []), ] opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor] Supervisor.start_link(children, opts) end ...

29. 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

30. Router ... pipeline :api_client_id do plug :header_required, "x-consumer-metadata" plug :client_id_exists end ... scope "/api", EHealth.Web do pipe_through [:api, :api_client_id] # Legal Entities get "/legal_entities/:id", LegalEntityController, :show patch "/legal_entities/:id/actions/mis_verify", LegalEntityController, :mis_verify patch "/legal_entities/:id/actions/nhs_verify", LegalEntityController, :nhs_verify patch "/legal_entities/:id/actions/deactivate", LegalEntityController, :deactivate # Employees get "/employees/:id", EmployeeController, :show scope "/employees" do pipe_through [:client_context_list] patch "/:id/actions/deactivate", EmployeeController, :deactivate end ...

31. Controllers & Actions • Controllers provide functions, called actions, to handle requests • Actions: • prepare data and pass it into views • invoke rendering via views • perform redirects

32. Controllers & Actions defmodule EHealth.Web.LegalEntityController do use EHealth.Web, :controller ... action_fallback EHealth.Web.FallbackController def show(%Plug.Conn{req_headers: req_headers} = conn, %{"id" => id}) do with {:ok, legal_entity, security} <- API.get_legal_entity_by_id(id, req_headers) do conn |> assign_security(security) |> render("show.json", legal_entity: legal_entity) end end def mis_verify(%Plug.Conn{req_headers: req_headers} = conn, %{"id" => id}) do with {:ok, legal_entity} <- API.mis_verify(id, get_consumer_id(req_headers)) do render(conn, "show.json", legal_entity: legal_entity) end end ...

33. Web Interface Entrypoint defmodule HelloPhoenixWeb do def controller do quote do use Phoenix.Controller, namespace: HelloPhoenixWeb import Plug.Conn import HelloPhoenixWeb.Router.Helpers import HelloPhoenixWeb.Gettext end end def view do ... end def router do ... end def channel do ... end ...

34. Fallback Controller action_fallback(plug) • Registers 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. web/controllers/fallback_controller.ex defmodule EHealth.Web.FallbackController do ... use EHealth.Web, :controller require Logger def call(conn, {:error, json_schema_errors}) when is_list(json_schema_errors) do conn |> put_status(422) |> render(EView.Views.ValidationError, "422.json", %{schema: json_schema_errors}) end def call(conn, {:error, errors, :query_parameter}) when is_list(errors) do conn |> put_status(422) |> render(EView.Views.ValidationError, "422.query.json", %{schema: errors}) end def call(conn, {:error, {:"422", error}}) do conn |> put_status(422) |> render(EView.Views.Error, :"400", %{message: error}) end ...

35. Views • Defines the view layer of a Phoenix application • Render templates • Define helper functions, available in templates, to decorate data for presentation

36. • Phoenix assumes a strong naming convention from controllers to views to the templates they render. • The PageController requires a PageView to render templates in the lib/hello_phoenix_web/ templates/page directory. Templates

37. Web Interface Entrypoint defmodule HelloPhoenixWeb do def controller do ... end def view do quote do use Phoenix.View, root: "lib/hello_phoenix_web/templates", namespace: HelloPhoenixWeb # Import convenience functions from controllers import Phoenix.Controller, only: [get_flash: 2, view_module: 1] # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML import HelloPhoenixWeb.Router.Helpers import HelloPhoenixWeb.ErrorHelpers import HelloPhoenixWeb.Gettext end end ...

38. View Module defmodule HelloPhoenixWeb.PageView do use HelloPhoenixWeb, :view end Because we have defined the template root to be "lib/hello_phoenix_web/templates", Phoenix.View will automatically load all templates at “lib/hello_phoenix_web/ templates/page” and include them in the HelloPhoenixWeb.PageView. lib/hello_phoenix_web/controllers/page_controller.ex ... def index(conn, _params) do render conn, "index.html" end ...

39. EEx Templates • EEx is the default template system in Phoenix • EEx module receives a template path and transforms its source code into Elixir quoted expressions • 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”

40. HTML EEx Template Hello <%= @name %> <h3>Keys for the conn Struct</h3> <%= for key <- connection_keys @conn do %> <p><%= key %></p> <% end %>

41. JSON - Controller defmodule EHealth.Web.DictionaryController do … alias EHealth.Dictionaries alias EHealth.Dictionaries.Dictionary action_fallback EHealth.Web.FallbackController def index(conn, params) do with {:ok, dictionaries} <- Dictionaries.list_dictionaries(params) do render(conn, "index.json", dictionaries: dictionaries) end end def update(conn, %{"name" => name} = dictionary_params) do with {:ok, %Dictionary{} = dictionary} <- Dictionaries.create_or_update_dictionary(name, dictionary_params) do render(conn, "show.json", dictionary: dictionary) end end End

42. JSON - View defmodule EHealth.Web.DictionaryView do use EHealth.Web, :view alias EHealth.Web.DictionaryView def render("index.json", %{dictionaries: dictionaries}) do render_many(dictionaries, DictionaryView, "dictionary.json") end def render("show.json", %{dictionary: dictionary}) do render_one(dictionary, DictionaryView, "dictionary.json") end def render("dictionary.json", %{dictionary: dictionary}) do %{ name:, values: dictionary.values, labels: dictionary.labels, is_active: dictionary.is_active } end end

43. Channels • Manage sockets for easy real-time communication • 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".

44. Channels hello_phoenix/lib/hello_phoenix_web/endpoint.ex ... socket "/socket", HelloPhoenixWeb.UserSocket ... hello_phoenix/lib/hello_phoenix_web/channels/user_socket.ex defmodule HelloPhoenixWeb.UserSocket do use Phoenix.Socket ## Channels channel "room:*", HelloPhoenixWeb.RoomChannel ... Any topic coming into the router with the "room:" prefix would dispatch to HelloPhoenixWeb.RoomChannel

45. Channels - Socket hello_phoenix/lib/hello_phoenix_web/channels/user_socket.ex defmodule HelloPhoenixWeb.UserSocket do use Phoenix.Socket ## Channels channel "room:*", HelloPhoenixWeb.RoomChannel ## Transports transport :websocket, Phoenix.Transports.WebSocket # Socket params are passed from the client and can # be used to verify and authenticate a user. After # verification, you can put default assigns into # the socket that will be set for all channels, ie # # {:ok, assign(socket, :user_id, verified_user_id)} # # To deny connection, return `:error`. def connect(_params, socket) do {:ok, socket} end ...

46. Joining Channels defmodule HelloPhoenixWeb.RoomChannel do use 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

47. Channels: phoenix.js hello_phoenix/assets/js/socket.js import {Socket} from "phoenix" let socket = new Socket("/socket", {params: {token: window.userToken}}) socket.connect() // Now that you are connected, you can join channels with a topic: let channel ="topic:subtopic", {}) channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) .receive("error", resp => { console.log("Unable to join", resp) }) ...

48. Channels: phoenix.js … channel.push("new_msg", {body: “Hello world!”}) … … channel.on("new_msg", payload => { … }) …

49. Channels: Incoming Events defmodule HelloPhoenixWeb.RoomChannel do use Phoenix.Channel … def handle_in("new_msg", %{"body" => body}, socket) do broadcast! socket, "new_msg", %{body: body} {:noreply, socket} end end We can pattern match on the event names, like "new_msg", and then grab the payload that the client passed over the channel.

50. Channels: Intercepting Outgoing Events ... intercept ["user_joined"] def handle_out("user_joined", msg, socket) do if Accounts.ignoring_user?(socket.assigns[:user], msg.user_id) do {:noreply, socket} else push socket, "user_joined", msg {:noreply, socket} end end ... This callback will be called for every recipient of a message, so more expensive operations like hitting the database should be considered carefully before being included in handle_out/3

51. Channels: Socket Assigns Similar to connection structs, %Plug.Conn{}, it is possible to assign values to a channel socket. socket = assign(socket, :user, msg[“user”]) Sockets store assigned values as a map in socket.assigns. user = socket.assigns[:user]

52. • Ecto is a domain specific language for writing queries and interacting with databases in Elixir. • What's new in Ecto 2.1: in-ecto-2-0

53. Ecto • 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 - allows 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.

54. Repositories Via the repository, we can create, update, destroy and query existing database entries.

55. Repositories hello_phoenix/lib/hello_phoenix/application.ex … def start(_type, _args) do import Supervisor.Spec children = [ # Start the Ecto repository supervisor(HelloPhoenix.Repo, []), … hello_phoenix/lib/hello_phoenix/repo.ex defmodule HelloPhoenix.Repo do use Ecto.Repo, otp_app: :hello_phoenix end

56. Repositories: config hello_phoenix/config/dev.exs ... # Configure your database config :hello_phoenix, HelloPhoenix.Repo, adapter: Ecto.Adapters.Postgres, username: "postgres", password: "postgres", database: "hello_phoenix_dev", hostname: "localhost", pool_size: 10 ... A repository needs an adapter and credentials to communicate to the database. Configuration for the Repo usually defined in your app config.

57. Schema Schemas allows developers to define the shape of their data.

58. Schema defmodule Weather do use Ecto.Schema # weather is the DB table schema "weather" do field :city, :string field :temp_lo, :integer field :temp_hi, :integer field :prcp, :float, default: 0.0 end end An :id field with type :id (:id means :integer) is generated by default, which is the primary key of the Schema.

59. Schema By defining a schema, Ecto automatically defines a struct with the schema fields: iex> weather = %Weather{temp_lo: 30} iex> weather.temp_lo 30 By defining a schema, Ecto automatically defines a struct with the schema fields: iex> weather = %Weather{temp_lo: 0, temp_hi: 23} iex> weather = Repo.insert!(weather) %Weather{...} iex> 1

60. Schema After persisting weather to the database, it will return a new copy of %Weather{} with the primary key (the id) set. We can use this value to interact with the repository: # Get the struct back iex> weather = Repo.get Weather, 1 %Weather{id: 1, ...} # Delete it iex> Repo.delete!(weather) %Weather{...}

61. Changesets Changesets allow developers to filter, cast, and validate changes before we apply them to the data. Changesets are also capable of transforming database constraints, like unique indexes and foreign key checks, into errors.

62. Changesets defmodule User do use Ecto.Schema import Ecto.Changeset schema "users" do field :name field :email field :age, :integer end def changeset(user, params %{}) do user |> cast(params, [:name, :email, :age]) |> validate_required([:name, :email]) |> validate_format(:email, ~r/@/) |> validate_inclusion(:age, 18..100) end end

63. Changesets Once a changeset is built, it can be given to functions like insert and update in the repository that will return an :ok or :error tuple changeset = User.changeset(%User{}, %{name: "Ivan", age: 30}) case Repo.update(changeset) do {:ok, user} -> # user updated {:error, changeset} -> # an error occurred end

64. Changesets We can easily provide different changesets for different use cases def registration_changeset(user, params) do # Changeset on create end def update_changeset(user, params) do # Changeset on update end

65. Query Ecto allows you to write queries in Elixir and send them to the repository, which translates them to the underlying database.

66. Query: predefined Schema import Ecto.Query, only: [from: 2] query = from u in User, where: u.age > 18 or is_nil(, select: u # Returns %User{} structs matching the query Repo.all(query)

67. Query: directly against a table query = from u in "users", where: u.age > 18 or is_nil(, select: %{name:, age: u.age} # Returns maps as defined in select Repo.all(query)

68. Query: accessing params values # min = 33 def min_age(min) do from u in User, where: u.age > ^min end # min = "35" Repo.all(from u in "users", where: u.age > type(^age, :integer), select:

69. Query: fragments def unpublished_by_title(title) do from p in Post, where: is_nil(p.published_at) and fragment("lower(?)", p.title) == ^title end # PostgreSQL’s JSON/JSONB data type with fragments fragment("?->>? ILIKE ?",, "key_name", ^some_value)

70. Ecto.Multi • Ecto.Multi is a data structure for grouping multiple Repo operations. • Ecto.Multi makes it possible to pack operations that should be performed in a single database transaction and gives a way to introspect the queued operations without actually performing them. • Each operation is given a name that is unique and will identify its result in case of success or failure. • All operations will be executed in the order they were added.

71. Ecto.Multi defmodule PasswordManager do alias Ecto.Multi def reset(account, params) do |> 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 end

72. Ecto.Multi result = Repo.transaction(PasswordManager.reset(account, params)) case result do {:ok, %{account: account, log: log, sessions: sessions}} -> # Operation was successful, 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 (like changeset for operations on changesets) to prepare a # proper response. We also get access to the results of any operations # that succeeded before the indicated operation failed. However, any # successful operations would have been rolled back. end

73. Phoenix-Ecto Contexts $ mix phx.gen.html Accounts User users name:string username:string:unique $ mix phx.gen.context Accounts Credential credentials email:string:unique user_id:references:users

74. Accounts Context defmodule HelloPhoenix.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false alias HelloPhoenix.Repo alias HelloPhoenix.Accounts.User def list_users do Repo.all(User) end def get_user!(id), do: Repo.get!(User, id) def create_user(attrs %{}) do %User{} |> User.changeset(attrs) |> Repo.insert() end def update_user(%User{} = user, attrs) do user |> User.changeset(attrs) |> Repo.update() end def delete_user(%User{} = user) do Repo.delete(user) end def change_user(%User{} = user) do User.changeset(user, %{}) end alias HelloPhoenix.Accounts.Credential def list_credentials do Repo.all(Credential) end def get_credential!(id), do: Repo.get! (Credential, id) def create_credential(attrs %{}) do %Credential{} |> Credential.changeset(attrs) |> Repo.insert() end def update_credential(%Credential{} = credential, attrs) do credential |> Credential.changeset(attrs) |> Repo.update() end def delete_credential(%Credential{} = credential) do Repo.delete(credential) end def change_credential(%Credential{} = credential) do Credential.changeset(credential, %{}) end end

75. THANK YOU! Yurii Bodarev @bodarev_yurii

Add a comment