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.

Ad005fac83baa60843ddf2bc3bc8fe93?s=128

Stefan Wintermeyer

February 07, 2019
Tweet

Transcript

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

  2. I am a software firefighter and architect for hire. I

    see a lot of projects which became unmaintainable over the years. @wintermeyer
  3. I am a Rails Dinosaur My first Rails book. My

    latest Rails book. @wintermeyer
  4. This talk is for normal people. Not for rock stars!

    I will not dive into religious wars. BTW: vim is better than emacs.
  5. Frameworks to develop web applications. @wintermeyer Rails Phoenix Hanami https://rubyonrails.org

    https://phoenixframework.org http://hanamirb.org
  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
  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
  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
  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
  10. @dhh @chris_mccord @jodosha They all worked with Rails in the

    past. @josevalim
  11. Ruby vs. Elixir

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

    happiness. @wintermeyer
  13. Elixir was created in 2011 by José Valim to be

    a real concurrent language. @wintermeyer
  14. Don’t let the Elixir syntax fool you into believing it’s

    easy to learn. @wintermeyer
  15. A Ruby programmer needs quite some time to learn Elixir.

    Functional programming is a totally different ball game. @wintermeyer
  16. Elixir’s biggest advantages are speed, scalability and hot deployments. @wintermeyer

  17. Hot Deployments! ZERO DOWNTIME! @wintermeyer No need to fire up

    a million Docker instances and some fancy HA Proxy setup.
  18. Let’s compare the Frameworks.

  19. Continuity for Developers Do you remember how painful Rails upgrades

    used to be?
  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
  21. 15 Minute Blog in 2019 @wintermeyer The 2005 grandfather: https://www.youtube.com/watch?v=Gzj723LkRJY

  22. Ruby on Rails @dhh

  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
  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
  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
  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
  27. None
  28. Phoenix Framework @chris_mccord

  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
  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
  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
  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
  33. None
  34. Hanami @jodosha

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

  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
  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
  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
  39. $ hanami new my_blog $ cd my_blog $ bundle $

    hanami generate model post $ hanami generate action web posts#index --url="/posts" @wintermeyer
  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
  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
  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
  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
  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

  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
  46. None
  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
  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
  49. Luca Guidi considers scaffolding an anti-pattern. @jodosha

  50. I disagree! @wintermeyer

  51. Why is scaffolding important? @wintermeyer

  52. Easy and fast entry for newbies. @wintermeyer When you try

    a new framework you want to see results within the first 20 minutes.
  53. For teams scaffolding is a big time saver and improves

    code quality by creating a base line. @wintermeyer
  54. It’s a power tool for refactoring. @wintermeyer If you don’t

    use it often you probably haven’t customized it yet.
  55. Over time Rails became easier and easier to use. That’s

    a major factor for it’s success. @wintermeyer
  56. Hanami is not there. (yet?) @wintermeyer

  57. But Phoenix is. @wintermeyer

  58. Stefan, tell us more about those customizable scaffold generators! "Never

    send a human to do a machine’s job." by Agent Smith
  59. Ruby on Rails @dhh

  60. Example: model generator

  61. None
  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".
  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
  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.
  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.
  66. Phoenix Framework @chris_mccord

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

  68. None
  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
  70. Hanami @jodosha

  71. You can’t.

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

    help?
  73. Stackoverflow
 Questions GitHub
 Contributors Ruby on Rails 302,236 3.742 Phoenix

    Framework 3,030 693 Hanami 60 140 @wintermeyer
  74. Twitter Followers @dhh 324 K @chris_mccord 13.6 K @jodosha 2.672

    @wintermeyer
  75. What should I use?

  76. Use Phoenix if speed is paramount. Technically it is the

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

    yet. Technically it is the best choice!
  78. Use Rails for all other cases.

  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')
  80. Thank you! Stefan Wintermeyer <sw@wintermeyer-consulting.de> @wintermeyer www.wintermeyer-consulting.de