Tricks that Rails didn't tell you about @ RailsConf 2014

Tricks that Rails didn't tell you about @ RailsConf 2014

77237b97a465ae5a293ad323b7296837?s=128

Carlos Antonio

April 23, 2014
Tweet

Transcript

  1. Tricks that Rails didn’t tell you about

  2. Carlos Antonio @cantoniodasilva

  3. ~6 years on Rails

  4. I’ve participated in several projects with different contexts

  5. I’ve seen code

  6. I’ve seen complex code

  7. I’ve seen duplicated code

  8. I’ve seen Rails code reimplemented in apps

  9. How often do you see them? All the time. They're

    everywhere.
  10. You get used to it after some time

  11. And that’s a big problem People are afraid to deal

    with code like that.
  12. Rails tricks / features

  13. That might help you refactor existing code or improve your

    developer life a bit
  14. None
  15. None
  16. Web Development Consultancy Mainly focused in Ruby and Rails Based

    in São Paulo - Brazil
  17. None
  18. ❤️

  19. plataformatec/devise

  20. plataformatec/simple_form

  21. elixir-lang/elixir

  22. Rails Core Team ! Carlos Antonio

  23. Rails Core Team José Valim Carlos Antonio Rafael França

  24. We use pull requests and code review All the f*cking

    time.
  25. We do cross project code review Sometimes, when the time

    allows us.
  26. And it’s usual to learn new things in the process

    We’re always learning, right?
  27. None
  28. None
  29. None
  30. None
  31. None
  32. ಠ_ಠ I had to do this talk after this look

    of disapproval
  33. Rails tricks / features Not used by many Not very

    well known and / or documented
  34. What we will see: Active Record Active Model Action Mailer

    Action View Action Controller Bonus Stuff
  35. Active Record

  36. migrations: change column null class ChangeNameToNotNullOnAuthors < ActiveRecord::Migration def up

    change_column :authors, :name, :string, null: false end ! def down change_column :authors, :name, :string, null: true end end
  37. migrations: change column null class ChangeNameToNotNullOnAuthors < ActiveRecord::Migration def up

    change_column_null :authors, :name, false end ! def down change_column_null :authors, :name, true end end
  38. migrations: change column null class ChangeNameToNotNullOnAuthors < ActiveRecord::Migration def change

    change_column :authors, :name, :string, null: false end end # => ActiveRecord::IrreversibleMigration ! class ChangeNameToNotNullOnAuthors < ActiveRecord::Migration def change change_column_null :authors, :name, false end end
  39. migrations: change column default class ChangeStatusDefaultOnArticles < ActiveRecord::Migration def up

    change_column :articles, :status, :string, default: 'draft' end ! def down change_column :articles, :status, :string, default: 'published' end end
  40. migrations: change column default class ChangeStatusDefaultOnArticles < ActiveRecord::Migration def up

    change_column_default :articles, :status, 'draft' end ! def down change_column_default :articles, :status, 'published' end end
  41. migrations: change column default class ChangeStatusDefaultOnArticles < ActiveRecord::Migration def change

    change_column :articles, :status, :string, default: 'draft' end end # => ActiveRecord::IrreversibleMigration ! class ChangeStatusDefaultOnArticles < ActiveRecord::Migration def change change_column_default :articles, :status, 'draft' end end # => ActiveRecord::IrreversibleMigration
  42. Relation#merge class Author < ActiveRecord::Base has_many :articles ! scope :with_draft_articles,

    -> { uniq.joins(:articles). where(articles: { status: 'draft' }) } end
  43. Relation#merge >> Author.with_draft_articles.to_sql ! SELECT DISTINCT "authors".* FROM "authors" INNER

    JOIN "articles" ON "articles"."author_id" = "authors"."id" WHERE "articles"."status" = 'draft'
  44. Relation#merge class Author < ActiveRecord::Base has_many :articles ! scope :with_draft_articles,

    -> { uniq.joins(:articles).merge(Article.draft) } end
  45. Relation#merge >> Author.with_draft_articles.to_sql ! SELECT DISTINCT "authors".* FROM "authors" INNER

    JOIN "articles" ON "articles"."author_id" = "authors"."id" WHERE "articles"."status" = 'draft'
  46. group counting >> Article.group(:status).count ! SELECT COUNT(*) AS count_all, status

    AS status FROM "articles" GROUP BY status ! => {"draft"=>7, "published"=>2}
  47. group counting: multiple attrs >> Article.group(:status, :category).count ! SELECT COUNT(*)

    AS count_all, status AS status, category AS category FROM "articles" GROUP BY status, category ! => {["draft", nil]=>5, ["draft", "Talks"]=>2, ["published", "News"]=>1, ["published", "Releases"]=>1}
  48. first! relation = Article.where(title: 'UNKNOWN') ! relation.first or raise(ActiveRecord::RecordNotFound, 'ZOMG

    not found') # => ZOMG Article found (ActiveRecord::RecordNotFound) ! relation.first! # => ActiveRecord::RecordNotFound
  49. last! relation = Article.where(title: 'UNKNOWN') ! relation.last or raise(ActiveRecord::RecordNotFound, 'ZOMG

    not found') # => ZOMG Article found (ActiveRecord::RecordNotFound) ! relation.last! # => ActiveRecord::RecordNotFound
  50. where.not class Article < ActiveRecord::Base scope :not_draft, -> { where

    'status != ?', 'draft' } end ! >> Article.not_draft.to_sql ! SELECT "articles".* FROM "articles" WHERE (status != 'draft')
  51. where.not class Article < ActiveRecord::Base scope :not_draft, -> { where.not

    status: 'draft' } end ! >> Article.not_draft.to_sql ! SELECT "articles".* FROM "articles" WHERE ("articles"."status" != 'draft')
  52. eager load strategies >> Article.includes(:author) ! SELECT "articles".* FROM "articles"

    ! SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (980190962, 298486374)
  53. eager load strategies >> Article.includes(:author).order('authors.name') ! SELECT "articles"."id" AS t0_r0

    ………… "authors"."updated_at" AS t1_r4 FROM "articles" LEFT OUTER JOIN "authors" ON "authors"."id" = “articles"."author_id" ORDER BY authors.name
  54. eager load strategies >> Article.eager_load(:author) ! SELECT "articles"."id" AS t0_r0

    ………… "authors"."updated_at" AS t1_r4 FROM “articles" LEFT OUTER JOIN "authors" ON "authors"."id" = “articles"."author_id"
  55. eager load strategies >> Article.preload(:author) ! SELECT "articles".* FROM “articles"

    ! SELECT "authors".* FROM “authors" WHERE "authors"."id" IN (980190962, 298486374)
  56. eager load strategies >> Article.preload(:author).joins(:author).order('authors.name') ! SELECT "articles".* FROM "articles"

    INNER JOIN "authors" ON "authors"."id" = "articles"."author_id" ORDER BY authors.name ! SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (980190962, 298486374)
  57. order :asc/:desc class Article < ActiveRecord::Base scope :recent, -> {

    order 'created_at DESC' } end ! >> Article.recent ! SELECT "articles".* FROM "articles" ORDER BY created_at DESC
  58. order :asc/:desc class Article < ActiveRecord::Base scope :recent, -> {

    order created_at: :desc } end ! >> Article.recent ! SELECT "articles".* FROM "articles" ORDER BY "articles"."created_at" DESC
  59. pluck >> Author.all.map(&:name) ! SELECT "authors".* FROM "authors" ! =>

    ["Author 2", "Author 1"]
  60. pluck >> Author.select(:name).map(&:name) ! SELECT "authors"."name" FROM "authors" ! =>

    ["Author 2", "Author 1"]
  61. pluck >> Author.pluck(:name) ! SELECT "authors"."name" FROM "authors" ! =>

    ["Author 2", "Author 1"]
  62. pluck: multiple attrs >> Author.pluck(:name, :active) ! SELECT "authors"."name", "authors"."active"

    FROM "authors" ! => [["Author 2", true], ["Author 1", true]]
  63. pluck: distinct values >> Author.distinct.pluck(:name) ! SELECT DISTINCT "authors"."name" FROM

    "authors" ! => ["Author 2", "Author 1"]
  64. to_param class Article < ActiveRecord::Base def to_param "#{id}-#{title.parameterize}" end end

    ! >> Article.all.map(&:to_param) => ["113629430-autoload", "281110143-rails-4-2-0-is-going-to-be-more-adequate", "298486374-rails-4-1-0-released", "980190962-rails-tricks"]
  65. to_param class Article < ActiveRecord::Base to_param :title end ! !

    ! >> Article.all.map(&:to_param) => ["113629430-autoload", "281110143-rails-4-2-0-is", "298486374-rails-4-1-0-released", "980190962-rails-tricks"]
  66. exists?/any? vs present? <% if @author.articles.exists? %> <%= render @author.articles

    %> <% else %> <p>No article found.</p> <% end %>
  67. exists?/any? vs present? >> author = Author.first ! >> author.articles.exists?

    ! SELECT 1 AS one FROM "articles" WHERE "articles"."author_id" = ? LIMIT 1 [["author_id", 298486374]] ! => true ! >> author.articles.each {} ! SELECT "articles".* FROM "articles" WHERE "articles"."author_id" = ? [["author_id", 298486374]]
  68. exists?/any? vs present? >> author = Author.first ! >> author.articles.present?

    ! SELECT "articles".* FROM "articles" WHERE "articles"."author_id" = ? [["author_id", 298486374]] ! => true ! >> author.articles.each {} ! # => already loaded, so no query
  69. benchmark class Article < ActiveRecord::Base def self.expensive_operation sleep 2 'did

    something very expensive' end end
  70. benchmark >> start = Time.now => 2014-04-21 10:48:30 -0500 >>

    Article.expensive_operation => "did something very expensive" >> finish = Time.now => 2014-04-21 10:48:32 -0500 >> finish - start => 2.02356
  71. benchmark >> ActiveRecord::Base.benchmark 'SUPER EXPENSIVE OPERATION' do ?> Article.expensive_operation >>

    end SUPER EXPENSIVE OPERATION (2000.9ms) => "did something very expensive"
  72. benchmark class Article < ActiveRecord::Base def self.expensive_operation benchmark 'SUPER EXPENSIVE

    OPERATION' do sleep 2 'did something very expensive' end end end
  73. None
  74. http://railsgirlssummerofcode.org/campaign/ And I got a free hug!

  75. Active Model

  76. ActiveModel::Model class EmailContact attr_accessor :name, :email, :message ! def deliver

    if name.present? && email.present? && message.present? puts 'deliver email' end end end
  77. ActiveModel::Model contact = EmailContact.new contact.name = 'Carlos' contact.email = 'carlos@example.com'

    contact.message = 'ZOMG' contact.deliver
  78. ActiveModel::Model <%= form_for @email_contact … %> ! <%= render @email_contact

    %>
  79. ActiveModel::Model class EmailContact extend ActiveModel::Naming extend ActiveModel::Translation include ActiveModel::Validations include

    ActiveModel::Conversion ! attr_accessor :name, :email, :message validates :name, :email, :message, presence: true ! def deliver if valid? puts 'deliver email' end end ! def persisted? false end end
  80. ActiveModel::Model class EmailContact include ActiveModel::Model ! attr_accessor :name, :email, :message

    validates :name, :email, :message, presence: true ! def deliver if valid? puts 'deliver email' end end end
  81. ActiveModel::Model contact = EmailContact.new( name: 'Carlos', email: 'carlos@example.com', message: 'ZOMG'

    ) contact.deliver
  82. ActiveModel::Model >> contact = EmailContact.new => #<EmailContact:0x007fb90e268350> ! >> contact.valid?

    => false ! >> contact.errors.full_messages.to_sentence => "Name can't be blank, Email can't be blank, and Message can't be blank"
  83. Action Mailer

  84. Mailer with i18n subject class Notifier < ActionMailer::Base default from:

    "from@example.com" ! # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.notifier.new_article.subject # def new_article @greeting = "Hi" ! mail to: "to@example.org" end end
  85. Mailer with i18n subject class Notifier < ActionMailer::Base default from:

    "from@example.com" ! ! ! ! ! ! def new_article @greeting = "Hi" ! mail to: "to@example.org", subject: I18n.t('subjects.new_article') end end
  86. Mailer with i18n subject en: subjects: new_article: 'New article just

    published.'
  87. Mailer with i18n subject en: notifier: new_article: subject: 'New article

    just published.'
  88. Mailer with i18n subject class Notifier < ActionMailer::Base default from:

    "from@example.com" ! def new_article @greeting = "Hi" ! mail to: "to@example.org" end end
  89. Mailer with i18n subject class Notifier < ActionMailer::Base default from:

    "from@example.com" ! def new_article @greeting = "Hi" ! mail to: "to@example.org", subject: I18n.t('subjects.new_article', title: 'Clarity is king') end end
  90. Mailer with i18n subject en: subjects: new_article: 'New article: %{title}.'

  91. Mailer with i18n subject en: notifier: new_article: subject: 'New article

    %{title}.'
  92. Mailer with i18n subject class Notifier < ActionMailer::Base default from:

    "from@example.com" ! def new_article @greeting = "Hi" ! mail to: "to@example.org", subject: default_i18n_subject(title: 'Clarity is king') end end
  93. Action View

  94. content_tag_for <%= content_tag_for :article, @articles do |article| %> <h2><%= article.title

    %></h2> <%= simple_format article.body %> <% end %>
  95. content_tag_for <article class="article" id="article_281110143"> <h2>Rails 4.2.0 is going to be

    more Adequate</h2> <p>Rails 4.2.0 is going to be made more Adequate by Tenderlove and Gorby Puff</p> </article> <article class="article" id="article_298486374"> <h2>Rails 4.1.0 Released</h2> <p>Rails 4.1.0 was just released before RailsConf</p> </article>
  96. content_tag_for <%= content_tag_for :section, @author do |author| %> <h1><%= author.name

    %></h1> … <% end %>
  97. content_tag_for <section class="author" id="author_298486374"> <h1>Author 2</h1> … </section>

  98. render empty collections <% if author.articles.exists? %> <%= render author.articles

    %> <% else %> <p>No article found.</p> <% end %>
  99. render empty collections <%= render(author.articles) || content_tag(:p, 'No article found.')

    %>
  100. partial locals <%= render article, full: true %> <%= render

    author.articles, full: false %> ! # article partial <%= content_tag_for :article, article do |article| %> <h2><%= article.title %></h2> ! <% if full %> <%= simple_format article.body %> <% else %> <%= truncate article.body %> <% end %> <% end %>
  101. partial locals <%= render article, full: true %> <%= render

    author.articles %> ! # article partial <%= content_tag_for :article, article do |article| %> <h2><%= article.title %></h2> ! <% if local_assigns[:full] %> <%= simple_format article.body %> <% else %> <%= truncate article.body %> <% end %> <% end %>
  102. truncate text <%= truncate article.body do %> <%= link_to 'read

    more...', article %> <% end %> ! ! Rails 4.2.0 is going to be ... <a href="/articles/281110143-rails-4-2-0-is"> read more… </a>
  103. I18n + html <%= link_to I18n.t('read_more'), article %> ! !

    en: read_more: 'read more…' ! ! <a href="/articles/281110143-rails-4-2-0-is"> read more… </a>
  104. I18n + html <%= link_to I18n.t('read_more'), article %> ! !

    en: read_more: 'read <b>more</b>…' ! ! <a href="/articles/281110143-rails-4-2-0-is"> read &lt;b&gt;more&lt;/b&gt;… </a>
  105. I18n + html <%= link_to raw(I18n.t('read_more')), article %> ! !

    en: read_more: 'read <b>more</b>…' ! ! <a href="/articles/281110143-rails-4-2-0-is"> read <b>more</b>… </a>
  106. I18n + html <%= link_to I18n.t(‘read_more_html'), article %> ! !

    en: read_more_html: 'read <b>more</b>…' ! ! <a href="/articles/281110143-rails-4-2-0-is"> read <b>more</b>… </a>
  107. benchmark <% benchmark 'Rendering articles' do %> <%= sleep(2); render

    author.articles %> <% end %> ! ! # logger Rendering articles (2005.1ms)
  108. Action Controller

  109. redirects with params get '/articles/:id' => redirect('/articles/%{id}/edit')

  110. exceptions handling $ ls public/*.html ! 404.html 422.html 500.html

  111. exceptions handling # config/application.rb ! # Use the app's own

    router to display error pages. config.exceptions_app = self.routes
  112. exceptions handling # config/routes.rb ! get "/404", to: "errors#not_found" get

    "/422", to: "errors#unprocessable_entity" get "/500", to: "errors#server_error"
  113. exceptions handling class ErrorsController < ApplicationController layout "error" ! def

    not_found render status: :not_found end ! def server_error render status: :server_error end ! def unprocessable_entity render status: :unprocessable_entity end end
  114. exceptions handling app/views/ errors/ not_found.html.erb server_error.html.erb unprocessable_entity.html.erb layouts/ error.html.erb

  115. Bonus Time!

  116. Console

  117. app >> app.author_path(Author.first) => “/authors/298486374" ! >> app.get _ Started

    GET "/authors/298486374" for 127.0.0.1 at 2014-04-22 19:31:04 -0500 Processing by AuthorsController#show as HTML … Rendered authors/show.html.erb within layouts/application (1.1ms) Completed 200 OK in 9ms (Views: 7.7ms | ActiveRecord: 0.2ms) => 200 ! >> app.class => ActionDispatch::Integration::Session
  118. helper >> helper.time_ago_in_words 30.days.ago => "about 1 month" ! >>

    helper.hello_rails_conf => "Hello RailsConf 2014!" ! >> helper.class => ActionView::Base
  119. application console helpers # lib/console_helpers.rb module ConsoleHelpers def a(id) Article.find(id)

    end end
  120. application console helpers # config/application.rb module RailsConf2014 class Application <

    Rails::Application console do require 'console_helpers' Rails::ConsoleMethods.send :include, ConsoleHelpers end end end
  121. application console helpers >> a(113629430) Article Load (0.1ms) SELECT "articles".*

    FROM "articles" WHERE "articles"."id" = ? LIMIT 1 [["id", 113629430]] ! => #<Article id: 113629430, …>
  122. console sandbox $ rails console >> Article.count => 4 !

    $ rails console --sandbox >> Article.create! title: 'I will be rolled back' => #<Article id: 980190968, title: "I will be rolled back", …> >> Article.count => 5 ! $ rails console >> Article.count => 4
  123. console sandbox $ rails console --sandbox … Any modifications you

    make will be rolled back on exit ! ! ! >> exit rollback transaction
  124. Annotations

  125. annotations class Author < ActiveRecord::Base # TODO: create inactive scope

    scope :active, -> { where active: true } end ! class Article < ActiveRecord::Base # FIXME: Fix for RailsConf scope :published, -> { where status: 'published' } end ! class AuthorsController < ApplicationController # OPTIMIZE: eager load stuff def show @author = Author.find(params[:id]) end end
  126. annotations $ rake notes ! app/controllers/authors_controller.rb: * [2] [OPTIMIZE] eager

    load stuff ! app/models/article.rb: * [9] [FIXME] Fix for RailsConf ! app/models/author.rb: * [4] [TODO] create inactive scope
  127. custom annotations class Notifier < ActionMailer::Base # RAILSCONF: Hello! def

    new_article end end
  128. custom annotations $ rake notes:custom ANNOTATION=RAILSCONF ! app/mailers/notifier.rb: * [4]

    Hello!
  129. Updating Rails

  130. rails update gem 'rails', '4.1.0' ! ! $ bundle update

  131. rails update $ rake rails:update identical config/boot.rb exist config conflict

    config/routes.rb ./railsconf2014-404/config/routes.rb? (enter "h" for help) [Ynaqdh] h Y - yes, overwrite n - no, do not overwrite a - all, overwrite this and all others q - quit, abort d - diff, show the differences between the old and the new h - help, show this help ./railsconf2014-404/config/routes.rb? (enter "h" for help) [Ynaqdh] a force config/routes.rb … exist config/initializers identical config/initializers/backtrace_silencers.rb create config/initializers/cookies_serializer.rb identical config/initializers/filter_parameter_logging.rb …
  132. rails update $ git status On branch master Changes not

    staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) ! modified: Gemfile modified: Gemfile.lock modified: config/environment.rb modified: config/environments/development.rb modified: config/environments/production.rb modified: config/environments/test.rb modified: config/initializers/mime_types.rb modified: config/initializers/session_store.rb modified: config/routes.rb ! Untracked files: (use "git add <file>..." to include in what will be committed) ! config/initializers/cookies_serializer.rb config/secrets.yml
  133. Every single part of Rails has hidden tricks

  134. They are sometimes hard to spot

  135. But they can make you much more productive

  136. So every time you start something new

  137. Before ever considering about pattern X, Y or Z

  138. Ask yourself: How Rails can help me?

  139. Search API Guidelines Google Stack Overflow …

  140. Ask Stack Overflow Rails Talk mailing list IRC …

  141. Follow Changelogs Release Notes Upgrading Guides Commits (if you can)

  142. And if you do find some hidden trick, remember…

  143. With great moustache! ! ! ! comes great responsibility

  144. Share Tweet Blog post Tell your friends …

  145. Contribute Back to Rails

  146. And please ping me too! ! @cantoniodasilva

  147. Thank you ❤️

  148. Stickers! @plataformatec

  149. Questions? Get one of these mugs (5 only :D)

  150. Tricks that Rails! didn’t tell you about Carlos Antonio -

    @cantoniodasilva