Upgrade to Pro — share decks privately, control downloads, hide ads and more …

一般的なWebサービスで必要そうな機能をPhoenixとEcto2.0で作りこんでみました

 一般的なWebサービスで必要そうな機能をPhoenixとEcto2.0で作りこんでみました

一般的なWebサービスで必要そうな機能をPhoenixとEcto2.0で作りこんでみました

Kenta Katsumata

April 19, 2016
Tweet

More Decks by Kenta Katsumata

Other Decks in Programming

Transcript

  1. ࣗݾ঺հ • ໊લɿউຢ݈ଠ • ࢓ࣄɿϑϦʔϥϯεͷࡶ৯ܥΤϯδχΞ • ݱࡏɿ(ג)ΞΧπΩ͞ΜͰ৽نࣄۀνʔϜͷΤϯδχΞ • झຯɿےτϨɾύʔςΟɾΫϥϒ׆ಈɾετϦʔτμϯεɾϥάϏʔ؍ ઓɾbokete(੕11000֫ಘܦݧ͋Γ)

    • ΩϟϦΞ·ͱΊ COBOL/Delphi/VC++/Perl/JavaScript/C#/VB.NET/Ruby/Java/PHP/ActionScript/lua/Python/Objective-C/Elixir Windows-SDK/MFC/STL/.NET Framework/Struts/CakePHP/Symfony/CodeIgniter/Zend Framwork/Ethna/Rails/Django/Phoenix SQL Server/Oracle/MySQL/Postgresql AWS(VPC/EC2/RDS/S3/CloudFront/Route53/IAM/SES/CLI) IIS/ActiveX(COM)/DirectShow/ActiveDirectory/Sharepoint/Biztalk/Exchange BIND/LDAP/DHCP/Sendmail/Postfix/apache/nginx/unicorn/Tomcat/Jenkins/Wercker/Chef/Capistrano/Gulp Xen/KVM/VMWare/Vagrant/Docker/LVS/memcached/Tokyo Tyrant/Kyoto Tycoon/Redis/MongoDB/Q4M/RabbitMQ Zabbix/nagios/Cacti/Munin/NewRelic/ElasticSearch/Mecab/Fluentd (αʔόʔαΠυ։ൃɾΠϯϑϥߏங؅ཧɾෛՙରࡦɾιʔγϟϧήʔϜ։ൃɾWindowsΞϓϦ։ൃɾiOS/AndroidΞϓϦ։ൃͳͲ৭ʑͱ)
  2. ͸͡Ίʹ (αϯϓϧͷ঺հ) https://github.com/kenta-aktsk/media_sample ҰൠతͳϝσΟΞܥͷWebαʔϏεͰඞཁʹͳΓͦ͏ͳԼهͷػೳΛ࣮૷ͯ͋͠Γ·͢ɻ • authentication/authorization (ueberauth/guardian) • social login

    (Github/Facebook/Twitter) • internationalization/localization/globalization (gettext/translator) • REST API (maru) • master/slave access to database (read_repos) • transaction (Ecto.Multi) • many to many relationship (Ecto 2.0) • pagination (scrivener) • using memcached as session storage (plug_session_memcached) • image file upload to S3 (arc) • send email via SES (mailman) • classification value (ex_enum) • service layer • database concurrent test • metaprogramming
  3. ͸͡Ίʹ (Phoenixͷੜ࢈ੑʹ͍ͭͯ) • Elixir/Phoenixͷັྗ͕ʮฒߦॲཧʯʮϦΞϧλΠϜWebʯͱ͍͏ลΓʹ͋Δͷ͸໌Β͔Ͱ͢ ͕ɺͦ͏͍ͬͨཁ͕݅ߴ౓ͳϨϕϧͰඞཁͱ͞ΕΔWebαʔϏε͸ͦΕ΄Ͳଟ͋͘Γ·ͤΜɻ • ҰൠతͳWebαʔϏεͷ։ൃʹ͓͍ͯ΋Elixir/Phoenix͸े෼ͳੜ࢈ੑΛൃشग़དྷΔͷ͔ɺͦΕ ͱ΋্هͷΑ͏ͳʮฒߦॲཧ/ϦΞϧλΠϜWebʯͱ͍ͬͨཁ͕݅ߴ౓ͳϨϕϧͰඞཁͱͳΔ αʔϏεͰͷΈ࢖༻͢΂͖ͳͷ͔ɺͦͷ఺Λ͸͖ͬΓ͓͖͍ͤͯͨ͞ͳͱɻ(۩ମతʹ͸ ʮRails/Django/ͦͷଞPHP౳LLݴޠͷϑϨʔϜϫʔΫʯͱൺֱͨ͠ࡍʹੜ࢈ੑʹͲͷఔ౓͕ࠩ

    ग़Δͷ͔ʯͱ͍͏఺ʹɺݱ࣌఺Ͱͷݸਓతͳ݁࿦Λग़͓͖͍ͯͨ͠) • ੈͷதͷWebαʔϏεͷେ൒͸ϝσΟΞܥͷWebαʔϏε(ϒϩά΍χϡʔε΍SNS౳)ɻݸਓ తʹ΋ࡢ೥ஸ౓ϝσΟΞܥͷWebαʔϏεΛ։ൃ͍ͯͨ͠ͷͰɺͦ͏͍ͬͨαʔϏεͰҰൠత ʹඞཁͱ͞ΕΔػೳΛ࡞Γ͜ΜͰΈΕ͹ɺੜ࢈ੑͷҧ͍Λ໌֬ʹ೺Ѳग़དྷΔ(ˍࠓޙΞΧπΩ͞ ΜͰϦϦʔε͞ΕΔ৽αʔϏεͰ΋ͦͷ··࢖͑Δ)ͱߟ͑·ͨ͠ɻ
  4. ͸͡Ίʹ (Rails/Django/PHPͱͷൺֱ) • ͱ͍͏͜ͱͰ͜ͷαϯϓϧΛ։ൃͯ͠ΈͨͷͰ͕͢ɺ࣮ࡍʹ৭ʑͱػೳΛ࡞Γ͜ΜͰΈͨײ૝ͱͯ͠ɺ Elixir/Phoenix͓Αͼͦͷपลͷύοέʔδ͸ʮRailsͷੜ࢈ੑʹ͸ٴ͹ͳ͍ʯ΋ͷͷʮ(͋Δఔ౓ͷϊ΢ϋ΢ ͕஝ੵ͞ΕΕ͹)ҰൠతͳWebαʔϏεͷ։ൃʹ͓͍ͯ΋े෼ͳੜ࢈ੑΛൃشͰ͖Δʯͱ͍͏ҹ৅Λ࣋ͪ· ͨ͠ɻ • ۩ମతʹ͸ɺ(աڈʹ৭ʑͳݴޠɾ৭ʑͳϑϨʔϜϫʔΫΛ࢖͖ͬͯͨܦݧΛݩʹൺֱ͢Δͱ)ɺElixir/ Phoenixͱपลύοέʔδͷੜ࢈ੑ͸ɺRails΍Django΄ͲͰ͸ͳ͍΋ͷͷɺPHPͷ֤छϑϨʔϜϫʔΫ

    (CakePHP΍CodeIgniter΍Zend Framework౳)ͱಉ౳΋͘͠͸ͦΕΛ্ճΔఔ౓ͷੜ࢈ੑ͸े෼ʹظ଴ग़ དྷΔ͔ͳͱɻ(΋ͪΖΜ։ൃऀ࣍ୈͰ͕͢) • (ͪͳΈʹ্ه͸PHP͕Python΍Rubyʹྼ͍ͬͯΔͱ͍͏ҙຯͰ͸͋Γ·ͤΜɻ͋͘·ͰաڈͷݶΒΕͨܦ ݧ͔Βͷݸਓతͳओ؍Ͱ͋ΓɺಛʹCRUDը໘ͷੜ࢈ੑ΍ύοέʔδͷ๛෋͞ɺύοέʔδ؅ཧπʔϧͷར ศੑʹؔ͢Δҹ৅ʹେ͖͘Өڹ͞Ε͍ͯ·͢ɻ࠷ۙ͸͋·ΓPHPΛ৮͓ͬͯΒͣɺ·ͨComposer͕ಋೖ͞ ΕͨޙͷPHPͷੜ࢈ੑͷ޲্ͷঢ়گʹؔͯ͠͸͋·Γྑ͘஌Γ·ͤΜͷͰɺͦͷ఺͸͝ཹҙ௖͚Ε͹ͱࢥ͍ ·͢)
  5. ͸͡Ίʹ (ݱ࣌఺Ͱͷݸਓత݁࿦) • ͭ·Γී௨ͷWebαʔϏεͷ։ൃʹશવ࢖͑ΔͷͰɺʮͦΖͦΖRailsͰͷ։ൃ΋๞͖͖ͯͨ͠ɺRails Ͱ։ൃͯ͠Δձࣾ͞Μଟ͍͔Β໨ཱͯͳ͍͠ɺؔ਺ܕݴޠ΍ͬͯΈ͍ͨ͠ɺকདྷతʹฒߦॲཧ΍ϦΞ ϧλΠϜWebతͳػೳ͕ඞཁʹͳͬͨ৔߹ͷ஌ݟ΋ஷΊ͓͖͍ͯͨ͠ɺੜ࢈ੑ΋ѱ͘ͳ͍Έ͍ͨͩ ͠ɺ࣍ͷϓϩδΣΫτͦΕ΄Ͳن໛΋େ͖͘ͳ͍͔ΒɺRails͡Όͳͯ͘Elixir/PhoenixͰ։ൃͯ͠ΈΑ ͏͔ͳʁ(ੜ࢈ੑѱ͗ͨ͢Γ໰୊͕සൃͨ͠ΒRailsͰॻ͖ͳ͓ͤ͹͍͍͠)ʯͱ͍͏ϊϦͰΦοέʔͰ ͸ͳ͍͔ͳͱɻ(΋ͪΖΜͦ͜Βล͸ࣗݾ੹೚Ͱ͕͢) •

    ͨͩ͠લड़ͷΑ͏ʹʮ͋Δఔ౓ͷϊ΢ϋ΢͕͋Ε͹ʯͱ͍͏͜ͱ͕લఏʹͳΓ·͢ɻͦͷϊ΢ϋ΢ʹ ؔͯ͠ʮϕετϓϥΫςΟεʯͱ·Ͱ͸͍͔ͳ͍·Ͱ΋ʮ͜ͷػೳ͸͜͏΍Ε͹࣮૷ग़དྷΔʯͱ͍͏ ࣮ྫΛఏࣔ͠ɺElixir/PhoenixΛ࠾༻͢ΔϋʔυϧΛԼ͍͛ͨͱ͍͏ͷ͕ɺ͜ͷαϯϓϧͷ໨తͷҰͭ Ͱ΋͋Γ·͢ɻ • ͱ͍͏͜ͱͰޙ͸ʮੋඇίʔυΛμ΢ϯϩʔυͯ͠৭ʑ͍ͬͯ͡Έͯ௖͚Ε͹ʯͱ͍͏͜ͱʹͳΔͷ Ͱ͕͢ɺͦΕ͚ͩͩͱൃදʹͳΒͳ͍ͷͰɺݸਓతʹॏཁ͔ͳͱࢥ͍ͬͯΔ෦෼ͷίʔυʹؔͯ͠؆ ୯ʹղઆͤͯ͞௖͖͍ͨͱࢥ͍·͢ɻ
  6. ଟݴޠରԠ routingͷઃఆ -> plugͰlocaleΛઃఆ -> controllerͰlocaleΛऔಘ -> model΍serviceʹϩέʔϧ Λ౉ͯ͠຋༁ςʔϒϧ͔ΒϨίʔυΛऔಘ(·ͨ͸ߋ৽) ->

    templateଆͰ຋༁ϨίʔυΛදࣔ ͱ͍͏ྲྀΕʹͳΓ·͢ɻ ޙ͔ΒଟݴޠରԠ༻ͷίʔυΛ௥Ճ͍ͯ͘͠ͷ͸݁ߏେม(templateͰ࢖༻͍ͯ͠Δpathϔϧύʔ ʹશͯҾ਺Λ௥Ճ͢Δඞཁ͕͋Δ౳)ͳͷͰɺग़དྷΔݶΓ࠷ॳ͔ΒରԠ͓͍ͯͨ͠ํ͕ྑ͍ͱࢥ͍ ·͢ɻ (ਓʹΑͬͯ͸શؔ͘৺ͷͳ͍τϐοΫ͔΋͠Ε·ͤΜ͕ɺݸਓతʹʮαʔϏε͸ͨ͘͞ΜͷϢʔ βʔʹ࢖ͬͯ΋Β͏͜ͱ͕લఏʯʮͨ͘͞ΜͷϢʔβʔʹ࢖ͬͯ΋Β͍͍ͨαʔϏεͰଟݴޠର Ԡ͠ͳ͍ͳΜͯ͋Γಘͳ͍Ͱ͢ΑͶ(ੈքਓޱ72ԯਓ/೔ຊਓ1.2ԯਓ/೔ຊࠃ಺ʹ΋֎ࠃਓଟ਺)ʯ ͱߟ͍͑ͯΔ͜ͱɺ͓Αͼίʔυͷ͋ͪͪ͜ʹଟݴޠରԠ༻ͷίʔυ͕ग़ͯ͘ΔͨΊɺ·ͣ͸͜ ΕΛઌʹઆ໌ͤͯ͞௖͖·͢)
  7. ଟݴޠରԠ (routing) # router.ex defmodule MediaSample.Router do use MediaSample.Web, :router

    pipeline :browser do ... plug MediaSample.Locale # url͔ΒlocaleΛ൑ఆ͢Δplug end # localeΛؚΉscopeΛ࡞੒͢Δɻ # ͨͩ͠Railsͷ"/(:locale)"ͷΑ͏ͳɺׅހͰғΉΦϓγϣφϧࢦఆॻ͕ࣜଘࡏ͠ͳ͍ͷͰɺ # ʮϩέʔϧ͕ࢦఆ͞Εͳ͚Ε͹σϑΥϧτͷϩέʔϧ͕ࢦఆ͞Εͨͱղऍ͢ΔʯΈ͍ͨͳ͜ͱ͸ग़དྷ·ͤΜɻ # (ৗʹϩέʔϧ෇͖ͷURLͰΞΫηεͯ͠΋Β͏ or ϩέʔϧ͕ࢦఆ͞Εͳ͚Ε͹ڧ੍తʹϦμΠϨΫτ͢Δඞཁ͕͋Δ) scope "/:locale" do scope "/admin", MediaSample.Admin, as: :admin do ... resources "/users", UserController end scope "/", MediaSample do ... resources "/entries", EntryController end end end
  8. ଟݴޠରԠ (plug) # plugs/locale.ex defmodule MediaSample.Locale do import Plug.Conn def

    init(opts), do: opts def call(conn, _opts) do supported_locales = MediaSample.Gettext.supported_locales cond do # ରԠ͍ͯ͠Δlocaleͷ৔߹͸GettextͷlocaleͱconnͷlocaleΛઃఆ͢Δɻ conn.params["locale"] in supported_locales -> conn |> assign_locale!(conn.params["locale"]) ... end end defp assign_locale!(conn, locale) do Gettext.put_locale(MediaSample.Gettext, locale) conn |> assign(:locale, locale) end end
  9. ଟݴޠରԠ (controller) # controllers/admin/user_controller.ex defmodule MediaSample.Admin.UserController do use MediaSample.Web, :admin_controller

    use MediaSample.LocalizedController # actionؔ਺Λoverrideͯ͠locale͕Ҿ਺Ͱ౉͞ΕΔΑ͏ʹ͍ͯ͠Δɻconn.assign͔Βऔಘͯ͠΋Α͍ɻ def index(conn, params, locale) do # ϩέʔϧʹରԠͨ͠translationςʔϒϧ͔ΒϨίʔυΛpreload͍ͯ͠Δɻޙड़ɻ page = User |> User.preload_all(locale) |> Repo.slave.paginate(params) render(conn, "index.html", users: page.entries, page: page) end def update(conn, %{"id" => id, "user" => user_params}, locale) do user = User |> User.preload_all(locale) |> Repo.slave.get!(id) changeset = User.changeset(user, user_params) # Service૚ͱEcto.MultiΛ࢖༻ͨ͠τϥϯβΫγϣϯɻޙड़ɻ case Repo.transaction(UserService.update(changeset, user_params, locale)) do {:ok, %{user: user, upload: _file}} -> conn |> put_flash(:info, gettext("%{name} updated successfully.", name: gettext("User"))) |> redirect(to: admin_user_path(conn, :show, locale, user)) |> halt ... end end end
  10. ଟݴޠରԠ (model) # models/user.ex defmodule MediaSample.User do use MediaSample.Web, :model

    schema "users" do ... has_one :translation, MediaSample.UserTranslation # ຋༁ςʔϒϧͱͷϦϨʔγϣϯΛઃఆ end @required_fields ~w(email name status user_type)a @optional_fields ~w(profile)a def changeset(user, params \\ %{}) do ... end # ຋༁ςʔϒϧͷpreloadઃఆɻuser_idҎ֎ʹlocaleΛࢦఆͯ͠preload͢Δඞཁ͕͋ΔͷͰԼهͷΑ͏ͳهड़͕ඞ ཁ def preload_all(query, locale) do from query, preload: [translation: ^UserTranslation.translation_query(locale)] end end
  11. ଟݴޠରԠ (຋༁༻model/migration) # models/user_translation.ex # ຋༁ςʔϒϧ༻ͷModelఆٛ defmodule MediaSample.UserTranslation do use

    MediaSample.Web, :model # ੿࡞ͷTranslatorύοέʔδΛ࢖༻ use Translator.TranslationModel, schema: "user_translations", belongs_to: MediaSample.User, required_fields: [:name], optional_fields: [:profile] end # create_user_translation.ex defmodule MediaSample.Repo.Migrations.CreateUserTranslation do use Ecto.Migration # ςʔϒϧߏ଄͸RailsͷglobalizeͰ࢖༻͞Ε͍ͯΔํࣜΛྲྀ༻ɻ def change do # utf8mb4ରԠ͍ͯ͠ΔͷͰoptionsʹԼهͷΑ͏ͳࢦఆ͕ඞཁ create table(:user_translations, options: "ROW_FORMAT=DYNAMIC") do add :user_id, references(:users, on_delete: :delete_all), null: false add :locale, :string, null: false add :name, :string, null: false add :profile, :text timestamps end create index(:user_translations, [:user_id, :locale], unique: true) end end
  12. ଟݴޠରԠ (template) # templates/admin/user/index.html.eex <h2><%= gettext "Listing users" %></h2> <table

    class="table"> ... <tbody> <%= for user <- @users do %> <tr> # ੿࡞ͷTranslatorύοέʔδͰఆٛ͞Ε͍ͯΔϔϧύʔؔ਺Λ࢖༻ <td><%= translate(user, :name) %></td> <td><%= translate(user, :profile) %></td> # ੿࡞ͷ۠෼஋؅ཧύοέʔδΛ࢖༻(಺෦ͰGettextʹରԠ͍ͯ͠Δ) <td><%= Status.get(user.status).text %></td> ... <% end %> </tbody> </table> <%= pagination_links @conn, @page, [@conn.assigns.locale], path: &admin_user_path/4 %> # routingઃఆʹ:localeΩʔΛ௥Ճ͍ͯ͠ΔͨΊɺpathϔϧύʔͷҾ਺͕શͯ1ͭ૿͑Δɻ(͜Ε͕݁ߏ໘౗) <%= link gettext("New"), to: admin_user_path(@conn, :new, @conn.assigns.locale) %>
  13. ଟݴޠରԠ (service) # services/user_service.ex defmodule MediaSample.UserService do use MediaSample.Web, :service

    alias MediaSample.{UserTranslation, UserImageUploader, Mailer} def update(changeset, params, locale) do Multi.new |> Multi.update(:user, changeset) # Multi.run಺Ͱ࣮ߦ͞ΕΔؔ਺ʹ͸ɺͦ͜·Ͱʹ࣮ߦ͞Εͨॲཧ݁Ռ͕౉͞ΕΔɻ # ԼهͷΑ͏ͳهड़Ͱinsertޙʹੜ੒͞ΕͨuserϞσϧΛऔಘग़དྷΔɻ |> Multi.run(:translation, &(UserTranslation.insert_or_update(Repo, &1[:user], params, locale))) # ը૾Ξοϓϩʔυʹؔͯ͠͸આ໌লུ(arcΛ࢖༻) |> Multi.run(:upload, &(UserImageUploader.upload(params["image"], &1))) end end
  14. ೝূ/ೝՄ(ueberauth) (config/routing) # config.exs config :ueberauth, Ueberauth, providers: [ github:

    {Ueberauth.Strategy.Github, [uid_field: "login"]}, facebook: {Ueberauth.Strategy.Facebook, []}, twitter: {Ueberauth.Strategy.Twitter, []}, identity: {Ueberauth.Strategy.Identity, [callback_methods: ["POST"]]} ] # router.ex defmodule MediaSample.Router do scope "/:locale" do scope "/admin", MediaSample.Admin, as: :admin do pipe_through [:browser] get "/auth/identity", SessionController, :new post "/auth/identity/callback", SessionController, :callback delete "/logout", SessionController, :delete end end end
  15. ೝূ/ೝՄ(ueberauth) (ϩάΠϯ༻controller) # controllers/admin/session_controller.ex defmodule MediaSample.Admin.SessionController do use MediaSample.Web, :admin_controller

    Enum.each Gettext.supported_locales, fn(locale) -> # router.exͰઃఆͨ͠routingʹରԠ͢Δbase_pathΛࢦఆ͢Δɻ # ଟݴޠରԠ͢Δ৔߹ɺϩέʔϧ͝ͱʹbase_pathΛࢦఆ͢Δඞཁ͕͋Δɻ plug Ueberauth, base_path: "/#{locale}/admin/auth" end def new(conn, _params, _locale) do render(conn, "new.html", callback_url: Helpers.callback_url(conn)) end # ϢʔβʔIDͱύεϫʔυ(ιʔγϟϧϩάΠϯͷ৔߹͸ೝূ৘ใ)͕͜ͷؔ਺ʹ౉͞ΕΔ def callback(%Plug.Conn{assigns: %{ueberauth_auth: auth}} = conn, _params, locale) do # ύεϫʔυൺֱ౳ඞཁͳॲཧΛߦ͏ case AdminUserAuthService.auth_and_validate(auth) do {:ok, admin_user} -> conn |> put_flash(:info, gettext("Signed in as %{name}", name: admin_user.name)) # Ϣʔβʔ৘ใΛηογϣϯʹอଘ |> AdminUserAuthService.login(admin_user) |> redirect(to: admin_page_path(conn, :index, locale)) |> halt ... end end end
  16. ೝূ/ೝՄ(ueberauth) (ଞͷcontroller) # controllers/admin/user_controller.ex defmodule MediaSample.Admin.UserController do use MediaSample.Web, :admin_controller

    ... end # web.ex defmodule MediaSample.Web do def admin_controller do quote do ... plug :check_logged_in # ϩάΠϯ͍ͯ͠ͳ͍৔߹͸ϩάΠϯը໘ʹϦμΠϨΫτͤ͞Δɻ # plugΛ࢖Θͳ͍ཧ༝͸ɺʮplug಺Ͱ͸pathϔϧύʔΛ࢖༻Ͱ͖ͳ͍ʯͨΊɻ # (pathϔϧύʔͷఆٛ͞ΕΔMediaSample.Router.HelpersϞδϡʔϧ͸ಈతʹੜ੒͞ΕΔϞδϡʔϧͰ͕͢ɺ # plug͕ίϯύΠϧ͞ΕΔλΠϛϯάͰ͸·ͩੜ੒͞Ε͍ͯͳ͍ͷͰࢀরͰ͖ͳ͍Α͏Ͱ͢) def check_logged_in(conn, _params) do session_paths = [ admin_session_path(conn, :new, conn.assigns.locale), admin_session_path(conn, :callback, conn.assigns.locale) ] if !(conn.request_path in session_paths) && !admin_logged_in?(conn) do conn |> redirect(to: admin_session_path(conn, :new, conn.assigns.locale)) |> halt else conn end end end end
  17. ೝূ/ೝՄ(guardian) (config/routing) # config.exs config :guardian, Guardian, issuer: "MediaSample", #

    JWTൃߦऀͷࣝผࢠ(ͱΓ͋͑ͣΞϓϦέʔγϣϯ໊Λࢦఆ͓͚ͯ͠͹ྑ͍Α͏Ͱ͢) ttl: {3, :days}, verify_issuer: true, serializer: MediaSample.GuardianSerializer # router.ex defmodule MediaSample.Router do pipeline :api_auth do # authorizationϔομʔͷBearerτʔΫϯΛνΣοΫ͢Δɻ # ਖ਼͍͠τʔΫϯ͕ઃఆ͞Ε͍ͯΕ͹ɺconn.privateʹ # claimsͱσίʔυͨ͠jwtτʔΫϯΛอ࣋͢Δɻ plug Guardian.Plug.VerifyHeader, realm: "Bearer" # Guardian.Plug.VerifyHeaderͷޙΖͰ࢖͏͜ͱ͕جຊɻ # conn.private͔Β஋Λऔಘͯ͠serializerΛ࢖ͬͯUser৘ใΛDB͔Βϩʔυ͠ɺ # औಘͨ͠UserϨίʔυΛconn.privateʹอ࣋͢Δɻ plug Guardian.Plug.LoadResource end scope "/:locale" do pipe_through [:api, :api_auth] forward "/api", MediaSample.API end end
  18. ೝূ/ೝՄ(guardian) (serializer) # lib/guardian_serializer.ex defmodule MediaSample.GuardianSerializer do @behaviour Guardian.Serializer alias

    MediaSample.{Repo, User} # τʔΫϯੜ੒࣌ʹ࢖༻͞Ε·͢ def for_token(user = %User{}), do: {:ok, "User:#{user.id}"} def for_token(_), do: {:error, "Unknown resource type"} # τʔΫϯ͔ΒUserΛϩʔυ͢Δࡍʹ࢖༻͞Ε·͢ def from_token("User:" <> id), do: {:ok, User |> User.valid |> Repo.get(String.to_integer(id))} def from_token(_), do: {:error, "Unknown resource type"} end
  19. ೝূ/ೝՄ(guardian) (ϩάΠϯ༻API) # apis/v1/session.ex defmodule MediaSample.API.V1.Session do use Maru.Router resource

    "/session" do params do use [:auth] end post "/create" do case UserAuthService.auth_and_validate(params) do {:ok, user} -> # User৘ใ͔ΒτʔΫϯΛੜ੒(͜͜ͰSerializer͕࢖ΘΕ·͢) {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token) conn |> put_status(:created) |> json(%{jwt: jwt}) ... end end end def unauthenticated(conn, _params) do conn |> put_status(:forbidden) |> json(%{error: "Not Authenticated"}) end end
  20. ೝূ/ೝՄ(guardian) (ଞͷAPI) # apis/v1/mypage/entry.ex defmodule MediaSample.API.V1.Mypage.Entry do use Maru.Router
 #

    conn.privateʹਖ਼͍͠τʔΫϯ͕ઃఆ͞Ε͍ͯΔ͔Ͳ͏͔Λ൑ఆ͠·͢ɻ # ਖ਼͍͠τʔΫϯ͕ઃఆ͞Ε͍ͯͳ͍ɺ΋͘͠͸claimsͷΩʔͷ஋͕Ұக͠ͳ͚Ε͹ɺ # handlerʹࢦఆͨ͠Ϟδϡʔϧͷunauthenticatedؔ਺͕ݺͼग़͞Ε·͢ɻ plug Guardian.Plug.EnsureAuthenticated, handler: MediaSample.API.V1.Session def check_user_permission!(user), do: ... def check_owner!(entry, user), do: ... resource "/entry" do params do use [:entry] end post "/save" do # conn.privateʹอ࣋͞Ε͍ͯΔUser৘ใΛϩʔυ͠·͢ɻ user = Guardian.Plug.current_resource(conn) check_user_permission!(user) locale = conn.assigns.locale multi = ... case Repo.transaction(multi) do {:ok, %{entry: entry}} -> conn |> json(render(EntryView, "show.json", entry: entry)) ... end end end end
  21. τϥϯβΫγϣϯ (Ecto.Multi) # services/user.ex defmodule MediaSample.UserService do … def insert(conn,

    changeset, params, locale) do # ը૾ͷΞοϓϩʔυ΍ϝʔϧૹ৴ॲཧ͸τϥϯβΫγϣϯ֎Ͱߦ͏΂͖ॲཧͰ͕͢ɺ # αϯϓϧ༻ͱ͍͏͜ͱͰτϥϯβΫγϣϯ಺Ͱ࣮ߦ͍ͯ͠·͢ɻ Multi.new |> Multi.insert(:user, changeset) |> Multi.run(:translation, &(UserTranslation.insert_or_update(Repo, &1[:user], params, locale))) |> Multi.run(:upload, &(UserImageUploader.upload(params["image"], &1))) |> Multi.run(:email, &(Mailer.deliver(Mailer.confirmation_email(conn, &1[:user].email, params["confirmation_token"])))) end end
  22. τϥϯβΫγϣϯ (Ecto.Multi) # controllers/admin/user_controller.ex defmodule MediaSample.Admin.UserController do … def create(conn,

    %{"user" => user_params}, locale) do changeset = User.changeset(%User{}, user_params) case Repo.transaction(UserService.insert(conn, changeset, user_params, locale)) do {:ok, %{user: user, upload: _file}} -> conn |> put_flash(:info, gettext("%{name} created successfully.", name: gettext("User"))) |> redirect(to: admin_user_path(conn, :show, locale, user)) |> halt # Τϥʔͷൃੜͨ͠ΦϖϨʔγϣϯʹΑͬͯfailed_valueʹฦ͞ΕΔσʔλͷܕ͕ҟͳΔͷͰऔΓѻ͍͕গʑ໘౗ɻ {:error, _failed_operation, failed_value, _changes_so_far} -> conn |> put_flash(:error, gettext("%{name} create failed.", name: gettext("User"))) |> render("new.html", changeset: extract_changeset(failed_value, changeset)) end end end
  23. DBͷϚελʔ/εϨʔϒରԠ (config) # ؀ڥ.exs config :media_sample, MediaSample.Repo, adapter: Ecto.Adapters.MySQL, hostname:

    "master", … config :media_sample, MediaSample.ReadRepo0, adapter: Ecto.Adapters.MySQL, hostname: "slave0", … config :media_sample, MediaSample.ReadRepo1, adapter: Ecto.Adapters.MySQL, hostname: "slave1", …
  24. DBͷϚελʔ/εϨʔϒରԠ (repo/application) # lib/media_sample/repo.ex defmodule MediaSample.Repo do @page_size 3 use

    Ecto.Repo, otp_app: :media_sample use Scrivener, page_size: @page_size # ReadReposύοέʔδΛ࢖༻ɻ # ͜ΕʹΑΓʮRepo.slaveʯͱ͍͏ؔ਺Λ࢖༻ͯ͠ɺεϨʔϒRepo͕ # ϥϯμϜʹબ୒͞ΕΔΑ͏ʹͳΔɻ use ReadRepos, page_size: @page_size end # media_sample.ex defmodule MediaSample do use Application def start(_type, _args) do import Supervisor.Spec, warn: false children = […] # supervisionπϦʔʹεϨʔϒ༻RepoΛ௥Ճ children = children ++ Enum.map(MediaSample.Repo.slaves, &supervisor(&1, [])) … end end
  25. DBͷϚελʔ/εϨʔϒରԠ (controller) # controllers/admin/admin_user_controller.ex defmodule MediaSample.Admin.AdminUserController do def index(conn, params)

    do # Repo.slaveؔ਺ܦ༝ͰεϨʔϒDBʹϥϯμϜʹΞΫηε page = AdminUser |> from |> Repo.slave.paginate(params) render(conn, "index.html", admin_users: page.entries, page: page) end def create(conn, %{"admin_user" => admin_user_params}) do changeset = AdminUser.changeset(%AdminUser{}, admin_user_params) # Insert/Updateॲཧͷ৔߹͸ϚελʔDBΛ࢖༻͢Δ case Repo.insert(changeset) do … end end end
  26. ۠෼஋؅ཧ(ex_enum) (definition/gettext) # status.ex defmodule MediaSample.Enums.Status do use ExEnum row

    id: 0, type: :invalid, text: "invalid" row id: 1, type: :valid, text: "valid" accessor :type translate :text end # priv/gettext/ja/LC_MESSAGES/default.po msgid "invalid" msgstr "ແޮ" msgid "valid" msgstr "༗ޮ"
  27. ۠෼஋؅ཧ(ex_enum) (template/model) # templates/admin/user/index.html.eex # ಺෦ͰGettextΛ࢖༻ͯ͠localeʹԠͨ͡จࣈྻΛදࣔग़དྷ·͢ɻ <td><%= Status.get(user.status).text %></td> #

    templates/admin/user/form.html.eex # selectλάͷoptionཁૉͱͯ͠࢖༻ग़དྷ·͢ɻ <%= select f, :status, Status.select([:text, :id]), class: "form-control" %> # models/user.ex defmodule MediaSample.User do def changeset(user, params \\ %{}) do user |> cast(params, @required_fields ++ @optional_fields) # όϦσʔγϣϯ༻ʹ۠෼஋ͷϦετΛऔಘग़དྷ·͢ɻ |> validate_inclusion(:status, Status.select(:id)) end def valid(query) do # ਺஋Ͱ͸ͳ໊͘લͰ(ԼهͰ͸`valid`)۠෼஋ʹΞΫηεग़དྷ·͢ɻ from u in query, where: u.status == ^Status.valid.id end end