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

Rails vs. Phoenix vs. Hanami

Rails vs. Phoenix vs. Hanami

This is the deck for a https://www.rubyfuza.org 2019 talk by Stefan Wintermeyer about the three frameworks Ruby on Rails, Phoenix Framework and Hanami.

Stefan Wintermeyer

February 07, 2019
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. Rails vs.
    Phoenix vs.
    Hanami
    by Stefan Wintermeyer
    @wintermeyer

    View Slide

  2. I am a software firefighter
    and architect for hire.
    I see a lot of projects which became unmaintainable
    over the years.
    @wintermeyer

    View Slide

  3. I am a Rails Dinosaur
    My first Rails book. My latest Rails book.
    @wintermeyer

    View Slide

  4. This talk is for normal people.
    Not for rock stars!
    I will not dive into religious wars. BTW: vim is better than emacs.

    View Slide

  5. Frameworks to develop web applications.
    @wintermeyer
    Rails Phoenix Hanami
    https://rubyonrails.org https://phoenixframework.org http://hanamirb.org

    View Slide

  6. Why use a Framework?
    • Structure and order within the project

    • No need to reinvent the wheel every day.
    Let others do the heavy lifting.


    • Documentation

    • Easier on boarding of new team members
    @wintermeyer

    View Slide

  7. The framework is your hammer.
    All problems have to become nails.
    And that’s a good thing because everybody in the team knows how to
    work with nails!
    @wintermeyer

    View Slide

  8. Inventor/Leader Sponsor
    Ruby Yukihiro Matsumoto Heroku
    Elixir José Valim Plataformatec
    Ruby on Rails
    David Heinemeier
    Hansson
    Basecamp
    Phoenix Framework Chris McCord DockYard
    Hanami Luca Guidi -
    @wintermeyer

    View Slide

  9. Version Command
    Ruby 2.5.3p105 ruby -v
    Elixir 1.8.0 elixir -v
    Ruby on Rails 5.2.2 rails -v
    Phoenix Framework 1.4.0 mix phx.new --version
    Hanami 1.3.1 hanami -v
    @wintermeyer

    View Slide

  10. @dhh
    @chris_mccord
    @jodosha
    They all worked with Rails
    in the past.
    @josevalim

    View Slide

  11. Ruby vs. Elixir

    View Slide

  12. Ruby was created in 1993
    by Yukihiro Matsumoto for
    developers happiness.
    @wintermeyer

    View Slide

  13. Elixir was created in 2011
    by José Valim to be a real
    concurrent language.
    @wintermeyer

    View Slide

  14. Don’t let the Elixir syntax
    fool you into believing it’s
    easy to learn.
    @wintermeyer

    View Slide

  15. A Ruby programmer needs
    quite some time to learn Elixir.
    Functional programming is a
    totally different ball game.
    @wintermeyer

    View Slide

  16. Elixir’s biggest advantages
    are speed, scalability and
    hot deployments.
    @wintermeyer

    View Slide

  17. Hot Deployments!
    ZERO DOWNTIME!
    @wintermeyer
    No need to fire up a million Docker instances and some fancy HA Proxy setup.

    View Slide

  18. Let’s compare the
    Frameworks.

    View Slide

  19. Continuity for Developers
    Do you remember how painful Rails upgrades used to be?

    View Slide

  20. Version
    How easy is an
    upgrade to the next
    version?
    Ruby on Rails 5.2.2
    Phoenix Framework 1.4.0
    Hanami 1.3.1 2.0 is a big change
    @wintermeyer

    View Slide

  21. 15 Minute Blog in 2019
    @wintermeyer
    The 2005 grandfather: https://www.youtube.com/watch?v=Gzj723LkRJY

    View Slide

  22. Ruby on Rails
    @dhh

    View Slide

  23. $ rails new my_blog
    $ cd my_blog
    $ rails g scaffold post title body:text
    $ rails db:migrate
    $ rails server
    # Open http://0.0.0.0:3000/posts
    @wintermeyer

    View Slide

  24. ├── app
    │ ├── assets
    │ │ ├── javascripts
    │ │ │ └── posts.coffee
    │ │ └── stylesheets
    │ │ ├── posts.scss
    │ │ └── scaffolds.scss
    │ ├── controllers
    │ │ └── posts_controller.rb
    │ ├── helpers
    │ │ └── posts_helper.rb
    │ ├── models
    │ │ └── post.rb
    │ └── views
    │ └── posts
    │ ├── _form.html.erb
    │ ├── _post.json.jbuilder
    │ ├── edit.html.erb
    │ ├── index.html.erb
    │ ├── index.json.jbuilder
    │ ├── new.html.erb
    │ ├── show.html.erb
    │ └── show.json.jbuilder
    ├── config
    │ └── routes.rb
    ├── db
    │ └── migrate
    │ └── 20190126153653_create_posts.rb
    └── test
    ├── controllers
    │ └── posts_controller_test.rb
    ├── fixtures
    │ └── posts.yml
    ├── models
    │ └── post_test.rb
    └── system
    └── posts_test.rb
    generated 20 files with 631 LoC

    View Slide

  25. class PostsController < ApplicationController
    before_action :set_post, only: [:show, :edit, :update, :destroy]
    # GET /posts
    # GET /posts.json
    def index
    @posts = Post.all
    end
    # GET /posts/1
    # GET /posts/1.json
    def show
    end
    # GET /posts/new
    def new
    @post = Post.new
    end
    # GET /posts/1/edit
    def edit
    end
    # POST /posts
    # POST /posts.json
    def create
    @post = Post.new(post_params)
    respond_to do |format|
    if @post.save
    format.html { redirect_to @post,
    notice: 'Post was successfully created.' }
    format.json { render :show, status: :created,
    location: @post }
    else
    format.html { render :new }
    format.json { render json: @post.errors,
    status: :unprocessable_entity }
    end
    end
    end
    # PATCH/PUT /posts/1
    # PATCH/PUT /posts/1.json
    def update
    respond_to do |format|
    if @post.update(post_params)
    format.html { redirect_to @post, notice: 'Post was successfull
    format.json { render :show, status: :ok, location: @post }
    else
    format.html { render :edit }
    format.json { render json: @post.errors, status: :unprocessabl
    end
    end
    end
    # DELETE /posts/1
    # DELETE /posts/1.json
    def destroy
    @post.destroy
    respond_to do |format|
    format.html { redirect_to posts_url, notice: 'Post was successfu
    format.json { head :no_content }
    end
    end
    private
    # Use callbacks to share common setup or constraints between actio
    def set_post
    @post = Post.find(params[:id])
    end
    # Never trust parameters from the scary internet, only allow the w
    def post_params
    params.require(:post).permit(:title, :body)
    end
    end
    app/controllers/posts_controller.rb
    def create
    @post = Post.new(post_params)
    respond_to do |format|
    if @post.save
    format.html { redirect_to @post,
    notice: 'Post was successfully created.' }
    format.json { render :show, status: :created,
    location: @post }
    else
    format.html { render :new }
    format.json { render json: @post.errors,
    status: :unprocessable_entity }
    end
    end
    end

    View Slide

  26. def create
    @post = Post.new(post_params)
    respond_to do |format|
    if @post.save
    format.html { redirect_to @post,
    notice: 'Post was successfully created.' }
    format.json { render :show, status: :created,
    location: @post }
    else
    format.html { render :new }
    format.json { render json: @post.errors,
    status: :unprocessable_entity }
    end
    end
    end
    @wintermeyer

    View Slide

  27. View Slide

  28. Phoenix Framework
    @chris_mccord

    View Slide

  29. $ mix phx.new my_blog
    $ cd my_blog
    # configure database in config/dev.exs (no SQLite )
    $ mix ecto.create
    $ mix phx.gen.html Blog Post posts title body:text
    # add "resources "/posts", PostController" to router.ex
    $ mix ecto.migrate
    $ mix phx.server
    # Open http://0.0.0.0:4000/posts
    @wintermeyer

    View Slide

  30. ├── lib
    │ ├── my_blog
    │ │ └── blog
    │ │ ├── blog.ex
    │ │ └── post.ex
    │ └── my_blog_web
    │ ├── controllers
    │ │ └── post_controller.ex
    │ ├── templates
    │ │ └── post
    │ │ ├── edit.html.eex
    │ │ ├── form.html.eex
    │ │ ├── index.html.eex
    │ │ ├── new.html.eex
    │ │ └── show.html.eex
    │ └── views
    │ └── post_view.ex
    ├── priv
    │ └── repo
    │ └── migrations
    ├── priv
    │ └── repo
    │ └── migrations
    │ └── 20190126174858_create_posts.exs
    └── test
    ├── my_blog
    │ └── blog
    │ └── blog_test.exs
    └── my_blog_web
    └── controllers
    └── post_controller_test.exs
    generated 12 files with 430 LoC

    View Slide

  31. defmodule MyBlogWeb.PostController do
    use MyBlogWeb, :controller
    alias MyBlog.Blog
    alias MyBlog.Blog.Post
    def index(conn, _params) do
    posts = Blog.list_posts()
    render(conn, "index.html", posts: posts)
    end
    def new(conn, _params) do
    changeset = Blog.change_post(%Post{})
    render(conn, "new.html", changeset: changeset)
    end
    def create(conn, %{"post" => post_params}) do
    case Blog.create_post(post_params) do
    {:ok, post} ->
    conn
    |> put_flash(:info, "Post created successfully.")
    |> redirect(to: Routes.post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
    end
    end
    def show(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    render(conn, "show.html", post: post)
    end
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: Routes.post_path(conn, :index))
    end
    end
    def edit(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    changeset = Blog.change_post(post)
    render(conn, "edit.html", post: post, changeset: chang
    end
    def update(conn, %{"id" => id, "post" => post_params}) d
    post = Blog.get_post!(id)
    case Blog.update_post(post, post_params) do
    {:ok, post} ->
    conn
    |> put_flash(:info, "Post updated successfully.")
    |> redirect(to: Routes.post_path(conn, :show, post
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "edit.html", post: post, changeset: c
    end
    end
    def delete(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    {:ok, _post} = Blog.delete_post(post)
    conn
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: Routes.post_path(conn, :index))
    end
    end
    lib/my_blog_web/controllers/post_controller.ex
    def create(conn, %{"post" => post_params}) do
    case Blog.create_post(post_params) do
    {:ok, post} ->
    conn
    |> put_flash(:info, "Post created successfully.")
    |> redirect(to: Routes.post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
    end
    end

    View Slide

  32. def create(conn, %{"post" => post_params}) do
    case Blog.create_post(post_params) do
    {:ok, post} ->
    conn
    |> put_flash(:info, "Post created successfully.")
    |> redirect(to: Routes.post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
    end
    end
    @wintermeyer

    View Slide

  33. View Slide

  34. Hanami
    @jodosha

    View Slide

  35. $ hanami new my_blog
    $ cd blog
    $ bundle
    @wintermeyer

    View Slide

  36. $ hanami generate model post
    create lib/hanami_blog/entities/post.rb
    create lib/hanami_blog/repositories/post_repository.rb
    create db/migrations/20190124185552_create_posts.rb
    create spec/hanami_blog/entities/post_spec.rb
    create spec/hanami_blog/repositories/post_repository_spec.rb
    @wintermeyer

    View Slide

  37. $ hanami g model post
    create lib/hanami_blog/entities/post.rb
    create lib/hanami_blog/repositories/post_repository.rb
    create db/migrations/20190124185552_create_posts.rb
    create spec/hanami_blog/entities/post_spec.rb
    create spec/hanami_blog/repositories/post_repository_spec.rb
    Hanami::Model.migration do
    change do
    create_table :posts do
    primary_key :id
    column :created_at, DateTime, null: false
    column :updated_at, DateTime, null: false
    end
    end
    end

    @wintermeyer

    View Slide

  38. $ hanami g model post
    create lib/hanami_blog/entities/post.rb
    create lib/hanami_blog/repositories/post_repository.rb
    create db/migrations/20190124185552_create_posts.rb
    create spec/hanami_blog/entities/post_spec.rb
    create spec/hanami_blog/repositories/post_repository_spec.rb
    Hanami::Model.migration do
    change do
    create_table :posts do
    primary_key :id
    column :title, String
    column :body, String
    column :created_at, DateTime, null: false
    column :updated_at, DateTime, null: false
    end
    end
    end

    @wintermeyer

    View Slide

  39. $ hanami new my_blog
    $ cd my_blog
    $ bundle
    $ hanami generate model post
    $ hanami generate action web posts#index --url="/posts"
    @wintermeyer

    View Slide

  40. $ hanami generate action web posts#index --url="/posts"
    create apps/web/controllers/posts/index.rb
    create apps/web/views/posts/index.rb
    create apps/web/templates/posts/index.html.erb
    create spec/web/controllers/posts/index_spec.rb
    create spec/web/views/posts/index_spec.rb
    insert apps/web/config/routes.rb
    @wintermeyer

    View Slide

  41. $ hanami new my_blog
    $ cd my_blog
    $ bundle
    $ hanami generate model post
    $ hanami generate action web posts#index --url="/posts"
    $ hanami generate action web posts#show --url="/posts"
    $ hanami generate action web posts#new --url="/posts/new"
    $ hanami generate action web posts#create --url="/posts"
    $ hanami generate action web posts#edit --url="/posts/edit"
    $ hanami generate action web posts#update --url="/posts"
    $ hanami generate action web posts#destroy —url="/posts"
    @wintermeyer

    View Slide

  42. ├── apps
    │ └── web
    │ ├── assets
    │ ├── config
    │ │ └── routes.rb
    │ ├── controllers
    │ │ └── posts
    │ │ ├── create.rb
    │ │ ├── destroy.rb
    │ │ ├── edit.rb
    │ │ ├── index.rb
    │ │ ├── new.rb
    │ │ ├── show.rb
    │ │ └── update.rb
    │ ├── templates
    │ │ └── posts
    │ │ ├── create.html.erb
    │ │ ├── destroy.html.erb
    │ │ ├── edit.html.erb
    │ │ ├── index.html.erb
    │ │ ├── new.html.erb
    │ │ ├── show.html.erb
    │ │ └── update.html.erb
    │ └── views
    │ └── posts
    │ ├── create.rb
    │ ├── destroy.rb
    │ ├── edit.rb
    │ ├── index.rb
    │ ├── new.rb
    │ ├── show.rb
    │ └── update.rb
    ├── config
    ├── db
    │ └── migrations
    │ └── 20190125105614_create_posts.rb
    ├── lib
    │ └── blog
    │ ├── entities
    │ │ └── post.rb
    │ ├── mailers
    │ └── repositories
    │ └── post_repository.rb
    └── spec
    ├── blog
    │ ├── entities
    │ │ └── post_spec.rb
    │ └── repositories
    │ └── post_repository_spec.rb
    └── web
    ├── controllers
    │ └── posts
    │ ├── create_spec.rb
    │ ├── destroy_spec.rb
    │ ├── edit_spec.rb
    │ ├── index_spec.rb
    │ ├── new_spec.rb
    │ ├── show_spec.rb
    │ └── update_spec.rb
    └── views
    └── posts
    ├── create_spec.rb
    ├── destroy_spec.rb
    ├── edit_spec.rb
    ├── index_spec.rb
    ├── new_spec.rb
    ├── show_spec.rb
    └── update_spec.rb
    @wintermeyer

    View Slide

  43. $ hanami generate action web posts#index --url="/posts"
    create apps/web/controllers/posts/index.rb
    create apps/web/views/posts/index.rb
    create apps/web/templates/posts/index.html.erb
    create spec/web/controllers/posts/index_spec.rb
    create spec/web/views/posts/index_spec.rb
    insert apps/web/config/routes.rb

    View Slide

  44. create apps/web/views/posts/index.rb
    create apps/web/templates/posts/index.html.erb
    create spec/web/controllers/posts/index_spec.rb
    create spec/web/views/posts/index_spec.rb
    insert apps/web/config/routes.rb

    View Slide

  45. module Web
    module Controllers
    module Posts
    class Create
    include Web::Action
    def call(params)
    end
    end
    end
    end
    end
    apps/web/controllers/posts/create.rb

    View Slide

  46. View Slide

  47. $ hanami new my_blog
    $ cd my_blog
    $ bundle
    $ hanami generate model post
    $ hanami generate action web posts#index --url="/posts"
    $ hanami generate action web posts#show --url="/posts"
    $ hanami generate action web posts#new --url="/posts/new"
    $ hanami generate action web posts#create --url="/posts"
    $ hanami generate action web posts#edit --url="/posts/edit"
    $ hanami generate action web posts#update --url="/posts"
    $ hanami generate action web posts#destroy —url="/posts"
    $ hanami db create
    $ hanami db migrate
    # put A LOT of Ruby code in the just generated files
    $ hanami server
    # Open http://localhost:2300
    @wintermeyer

    View Slide

  48. The 15 Minute Blog
    @wintermeyer
    Time (hh:mm) generated LoC
    Ruby on Rails 00:01 631
    Phoenix Framework 00:02 430
    Hanami 03:30 501

    View Slide

  49. Luca Guidi considers
    scaffolding an anti-pattern.
    @jodosha

    View Slide

  50. I disagree!
    @wintermeyer

    View Slide

  51. Why is scaffolding
    important?
    @wintermeyer

    View Slide

  52. Easy and fast entry
    for newbies.
    @wintermeyer
    When you try a new framework you want to see results within the
    first 20 minutes.

    View Slide

  53. For teams scaffolding is a
    big time saver and
    improves code quality by
    creating a base line.
    @wintermeyer

    View Slide

  54. It’s a power tool for
    refactoring.
    @wintermeyer
    If you don’t use it often you probably haven’t customized it yet.

    View Slide

  55. Over time Rails became easier
    and easier to use.
    That’s a major factor for it’s
    success.
    @wintermeyer

    View Slide

  56. Hanami is not there.
    (yet?)
    @wintermeyer

    View Slide

  57. But Phoenix is.
    @wintermeyer

    View Slide

  58. Stefan, tell us more about
    those customizable
    scaffold generators!
    "Never send a human to do a machine’s job." by Agent Smith

    View Slide

  59. Ruby on Rails
    @dhh

    View Slide

  60. Example:
    model generator

    View Slide

  61. View Slide

  62. Download
    https://raw.githubusercontent.com/rails/rails/master/
    activerecord/lib/rails/generators/active_record/
    model/templates/model.rb.tt
    and copy it to
    lib/templates/active_record/model/model.rb.tt
    It will be used for every "rails g model xyz" or "rails g scaffold xyz".

    View Slide

  63. <% module_namespacing do -%>
    class <%= class_name %> < <%= parent_class_name.classify %>
    <% attributes.select(&:reference?).each do |attribute| -%>
    belongs_to :<%= attribute.name %><%= ', polymorphic: true'
    if attribute.polymorphic? %><%= ', required: true' if
    attribute.required? %>
    <% end -%>
    <% attributes.select(&:token?).each do |attribute| -%>
    has_secure_token<% if attribute.name != "token" %> :<%=
    attribute.name %><% end %>
    <% end -%>
    <% if attributes.any?(&:password_digest?) -%>
    has_secure_password
    <% end -%>
    end
    <% end -%>
    lib/templates/active_record/model/model.rb.tt

    View Slide

  64. Example Enhancement
    <%- if attributes.map{ |a| a.name }.include?('position') -%>
    acts_as_list
    <% end -%>
    <% if attributes.map{ |a| a.name }.include?('name') -%>
    validates :name, presence: true
    def to_s
    name
    end
    <% end -%>
    Should be customized for each project.

    View Slide

  65. lib/templates/
    ├── active_record
    │ └── model
    │ └── model.rb.tt
    ├── erb
    │ └── scaffold
    │ ├── _form.html.erb.tt
    │ ├── edit.html.erb.tt
    │ ├── index.html.erb.tt
    │ ├── new.html.erb.tt
    │ └── show.html.erb.tt
    └── rails
    └── scaffold_controller
    └── controller.rb.tt
    Just google for the file names to find the default ones on github.com/rails.

    View Slide

  66. Phoenix Framework
    @chris_mccord

    View Slide

  67. The same concept as with
    Rails. Just done with Elixir.

    View Slide

  68. View Slide

  69. def create(conn, %{<%= inspect schema.singular %> => <%= schema.singular
    %>_params}) do
    case <%= inspect context.alias %>.create_<%= schema.singular %>(<%=
    schema.singular %>_params) do
    {:ok, <%= schema.singular %>} ->
    conn
    |> put_flash(:info, "<%= schema.human_singular %> created successfully.")
    |> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%=
    schema.singular %>))
    {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, "new.html", changeset: changeset)
    end
    end
    https://github.com/phoenixframework/phoenix/blob/master/priv/templates/
    phx.gen.html/controller.ex

    View Slide

  70. Hanami
    @jodosha

    View Slide

  71. You can’t.

    View Slide

  72. How big is the
    community/visibility?
    How easy can I get help?

    View Slide

  73. Stackoverflow

    Questions
    GitHub

    Contributors
    Ruby on Rails 302,236 3.742
    Phoenix Framework 3,030 693
    Hanami 60 140
    @wintermeyer

    View Slide

  74. Twitter Followers
    @dhh 324 K
    @chris_mccord 13.6 K
    @jodosha 2.672
    @wintermeyer

    View Slide

  75. What should I use?

    View Slide

  76. Use Phoenix if speed is
    paramount.
    Technically it is the best choice!

    View Slide

  77. Use Phoenix if your team
    doesn’t have (much?)
    Ruby knowledge yet.
    Technically it is the best choice!

    View Slide

  78. Use Rails for all other
    cases.

    View Slide

  79. Don’t use Hanami (for now).
    Let’s see what the future brings.
    "Nobody ever got fired for buying IBM".gsub(/buying IBM/, 'using Rails')

    View Slide

  80. Thank you!
    Stefan Wintermeyer
    @wintermeyer
    www.wintermeyer-consulting.de

    View Slide