Skip to content

Instantly share code, notes, and snippets.

@JamesYang76
Last active February 26, 2021 15:22
Show Gist options
  • Save JamesYang76/b7e090f5f10cf2d2004c5765d919db49 to your computer and use it in GitHub Desktop.
Save JamesYang76/b7e090f5f10cf2d2004c5765d919db49 to your computer and use it in GitHub Desktop.
rails_basic

Coding tips

refer to https://legacy.gitbook.com/book/spartchou/ruby-on-rails-basic/details

Save model

Do not use @post.user_id = current_user.id, and use save! instead of save unless check return value of save

class PostsController < ApplicationController
# @post.user_id = current_user.id and 
  def create
    @post = current_user.posts.build(params[:post])
    @post.save!
  end
end

class User < ActiveRecord::Base
  has_many :posts
end

use public_send

Unlike send, public_send can call only public method.
ppublic_send is recommended instead of send, but send is useful for unit test of private method

# process_date_format is private method
assert_equal importer.send(:process_date_format, "01/01/15"), "01-01-15"

process_date_format is private method

assert_equal importer.send(:process_date_format, "01/01/15"), "01-01-15"

scope access

Do not use Unscoped access, which could raise brakeman Unscoped Find exception.

# Bad Smell below....
def edit
  @post = Post.find(params[:id])
    if @post.user != current_user
      flash[:warning] = 'Access denied'
      redirect_to posts_url
  end
end

# should use below....
def edit
  # raise RecordNotFound exception (404 error) if not found
  @post = current_user.posts.find(params[:id]) 
  # or use pundit policy_scope, @post = policy_scope(Post).find(params[:id])
end

Eager loading to prevent n+1 queries

@libraries = Library.where(size: 'large').includes(:books) #has_many associations 
@books = Book.all.includes(:author) #belongs_to / has_one associations
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
# category with id 1 and eager load all of the associated articles, 
# the associated articles' tags and comments, and every comment's guest association.

batched finder for large data

Do not use User.all.each for large data, find_each or find_in_batches are useful.

# the application only finds 1,000 users once, yield them, then handle the next 1,000 users
User.find_each do |user|
  NewsLetter.weekly_deliver(user)
end

# 1,000 is the default batch size, you can use the :batch_size option to change it.
User.find_in_batches(:batch_size => 5000) do |users|
  users.each { |user| NewsLetter.weekly_deliver(user) }
end

Time.zone.now

Time.zone = "Alaska"
# Do not use below, because it show the local time
Time.now
Date.today
Date.today.to_time
Time.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")

# Use below
Time.zone.now
Time.zone.today
Time.current
2.hours.ago
Date.current
1.day.from_now
Time.zone.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone

ENV.fetch

Use ENV.fetch for environment variables instead of ENV[] so that unset environment variables are detected on deploy.

development:
  <<: *default
  database: energy-efficiency-funding_development
  username: <%= ENV.fetch("DB_USERNAME", "postgres") %>
  password: <%= ENV.fetch("DB_PASSWORD", "") %>
  host: <%= ENV.fetch("DB_HOST", "localhost") %>
  port: <%= ENV.fetch("DB_PORT", "5432") %>

flash vs flash.now

flash: redirection, for the next request flash.now: render template only, for the same request

class ClientsController < ApplicationController
  def create
    @client = Client.new(params[:client])
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

use right type for query

Query condition should be right type

# Bad Smell below....
request.auth_comments.where.not(id: [nil, ""]).each { ... }
# id is not string, but "" is used for where statement.

# before 5.2.3,  which is converted below sql statement.
=>
SELECT "auth_comments".* FROM "auth_comments" WHERE "auth_comments"."request_id" = $1
AND NOT (("auth_comments"."id" IS NULL OR "auth_comments"."id" IS NULL)) [["request_id", 1]]
# "auth_comments"."id" IS NULL is duplicated, but it works on 5.2.2.1

# since Rails 5.2.3, the code generated like blow..., 
# so it doest not work because "auth_comments"."id" = $2 ["id", nil]] 
=>
SELECT "auth_comments".* FROM "auth_comments" 
WHERE "auth_comments"."request_id" = $1 
AND NOT (("auth_comments"."id" = $2 OR "auth_comments"."id" IS NULL)) [["request_id", 1], ["id", nil]]

# finding records where string column is not null or empty is useful
Product.where.not(title: ["",nil]) 
#=>
SELECT "products".* FROM "products" 
WHERE NOT (("products"."title" = $1 OR "products"."title" IS NULL)) 
LIMIT $2  [["title", ""], ["LIMIT", 11]]


# Do not use NULL in query string because 1 = NULL is NULL also 1 != NULL is NULL in sql
Person.where.not(manager_id: "NULL")
Person.where("manager_id != NULL")
=> SELECT people.* FROM people  WHERE people.manager_id != NULL

Person.where.not(manager_id: nil)
=>  SELECT people.* FROM people WHERE people.manager_id IS NOT NULL

Use persisted? method in order to check a saved record instead of using query for id is null

# use persisted?
class AuthComment < ApplicationRecord
  scope :persisted, -> { select(&:persisted?) }
end

request.auth_comments.persisted.each {...}

Use new_record? method to check this object hasn’t been saved yet. Returns true if this object hasn’t been saved yet

 <%= f.submit @contact.new_record? ? "Save" : "Update", class:"btn btn-primary" %>

use destroy instead of delete

  • delete removes only delete current object record from db but not its associated children records from db.
  • destroy removes current object record from db and also its associated children record from db when having :dependent, and also can execute callback before_destroy, around_destroy, after_destroy
has_many :addresses, dependent: :destroy

use merge

class Book < ActiveRecord::Base
  belongs_to :author
  scope :available, ->{ where(available: true) }
end

class Author < ActiveRecord::Base
  has_many :books
  
  # without merge
  scope :with_available_books, joins(:books).where("books.available = ?", true)
  
  # with merge
  scope :with_available_books, joins(:books).merge(Book.available)
end

rescue_from

Use rescue_from instead of rescue in each acitons

# Bad Smell
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def edit
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  rescue ActiveRecord::RecordNotFound
    render_404
  end
end

class UserController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_404
  end

  # ...
end


# use rescue_from
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, :with => :render_404
  ...
  def render_404
    render template: "errors/404", status: :not_found
  end
end

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
  end

  def edit
    @post = Post.find(params[:id])
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy
  end
end

Render Collections

This will work if you have a _book.html.erb partial & use book as your variable inside that template.

# render calls to_partial_path method on each of our objects
# Models that inherit from ActiveRecord::Base will teturn a partial name based on the model's name 
Book.new.to_partial_path #=>"books/book"

# app/views/books/index.html.erb
<%= render @books %>

# app/views/books/_book.erb
<div class="book">
  <%= image_tag(book.cover) %>
  <div class="book-description">
    <h4><%= book.title %></h4>
    <%= book.author %>
  </div>
  <%= link_to "Read Book", book %>
</div>

route as option

 # config/routes.rb
 get 'contacts/index'
 =>
 # Prefix          Verb          URI Pattern               Controller#Action                        
 # contacts_index  GET      /contacts/index(.:format)      contacts#index
 
 # This will create contact_path and contact_url as named route helpers in your application. 
 # Calling contacts_path will return /contacts/index
 get 'contacts/index', as: 'contacts'
 # Pefix          Verb          URI Pattern               Controller#Action                        
 # contacts      GET      /contacts/index(.:format)      contacts#index

url vs path

path is relative while url is absolute.

employees_url => http://localhost:3000/employees
employees_path  # => /employees

#_path are generally used in views because hrefs are implicitly linked to the current URL. 
#_url is needed typically, with url redirects - redirect_to.

collection and memeber

# bad smell
get '/posts/:id/preview', to: 'posts#preview', as: "review_post"
get '/posts/search', to: 'posts#search', as: "search_posts"

# should use below...
resources :posts do
  get :preview, on: :member
  get :search,  on: :collection
end

form

In form, name attributes in each elements are params attributes.

  <form action="/contacts/create", method="post">
     <input type="hidden", name="authenticity_token" value="<%= form_authenticity_token %>">
     <label for="name" class="control-label col-md-3">Name</label>
     <input type="text" name="contact[name]">
     <input type="text" name="contact[company]">
  </form>  
       
 <!-- {
"authenticity_token"=>"nfh1IyqSkvC0qPRrGWCsrBSa8ZEimkkU6OpuqlYXTY0jmGkgWOeM0EsVhOtgjLy+xGAyEURfP3DpSH2Tlw/lrg==",
"contact"=>{"name"=>"test", "company"=>"test com"}, "..."=>""} -->
<%= form_for(@contact) do |f| %>
 <%= f.label :name, class: "control-label col-md-3" %>
 <%= f.text_field :name, class: "form-control" %>
  ...
 <%= f.text_field :company, class: "form-control" %>
<% end %>

# for update rails convert add input element for patch
<form accept-charset="UTF-8" action="/search" method="post">
  <input name="_method" type="hidden" value="patch" />
  ...
<% end %>  
<form method="post" action="/posts/<%= post.id %>" style='display: inline'>
  <input name= "_method" type="hidden" value="delete">
  <input type="submit" value="Delete" />
</form>

# On the server side, Rails is able to examine the "_method" value in the params to restore the semantics of the HTTP request.

For file upload, use multipart/form-data

 <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
 </form>
 
 <%= form_for(@contact, html: {multipart: true} ) do |f| %>
    <%= f.file_field :avatar %>
  ...   
 <% end %>   

error

 c = Contact.new(name:"test")
 c.save
 c.errors.any? #=> true
 c.errors.messages 
 #=> {:group=>["must exist"], :email=>["can't be blank"], :group_id=>["can't be blank"]} 
 c.errors.full_messages
 # => ["Group must exist", "Email can't be blank", "Group can't be blank"]

Use view helper in controller

module UsersHelper
  def full_name(user)
    user.first_name + user.last_name
  end
end


class UsersController < ApplicationController
  include UsersHelper
   
   def update
     ...
     redirect_to user_path(@user), notice: "#{full_name(@user) is successfully updated}"
   end
end

# or use helpers
class UsersController < ApplicationController
   def update
      notice = "#{helpers.full_name(@user) is successfully updated}"
      redirect_to user_path(@user), notice: notice
   end
end

Use controller method in view

class ApplicationController < ActionController::Base
helper_method :current_user, :logged_in?
  def logged_in?
   !!current_user
  end
end

select

<label for="group">Group</label>
<select name="contact[group_id]" id="group" class="form-control">
    <option value="">Select group</option>
     <%# Group.all.each do |group| %>
         <option value="<%#=group.id %>"><%#=group.name%></option>
     <%# end %>
 </select>
           
           
 <%= f.label :group_id, class: "control-label col-md-3" %>          
 <%= f.collection_select :group_id, Group.all, :id, :name, { prompt: "Select Group"} %>          

Use sandbox

In production, use --sandbox option for testing query

$ rails c --sandbox

Float Imprecision

0.2 + 0.1 == 0.3
==> false
0.2 + 0.1
=> 0.30000000000000004

Calling a javascript function

When a js function is called in view(html.erb), window.funcname = funcion()...

https://www.theodinproject.com/courses/ruby-on-rails/lessons/advanced-forms

atch : Used for updating an object partially, takes less bandwidth. put : Used for updating the whole object. delete : Used for removal of object.

Now the problem with these requests are that most of the browsers support only get and post. Rails fixed this problem by emulating other methods over POST with a hidden input named "_method". It tells the controller about which type of request is coming, even if the browser doesn't support them. For example if there is a form to update the user's entry and we need to use the patch request, the form will look like this:

form_tag(users_path, method: "patch") It will produce the following HTML:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment