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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

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

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

Avatar for Kenta Katsumata

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