Ruby on Rails


Rails Best Practices All Versions

1.0
1.1
1.2
2.0
2.3
3.0
3.1
3.2
4.0
4.1
4.2
5.0

This draft deletes the entire topic.

Introduction

Introduction

expand all collapse all

Examples

  • 20

    “Fat Model, Skinny Controller” refers to how the M and C parts of MVC ideally work together. Namely, any non-response-related logic should go in the model, ideally in a nice, testable method. Meanwhile, the “skinny” controller is simply a nice interface between the view and model.

    In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.

    Let’s look at a simple example. Say you have code like this:

    def index
      @published_posts = Post.where('published_at <= ?', Time.now)
      @unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
    end
    

    You can change it to this:

    def index
      @published_posts = Post.published
      @unpublished_posts = Post.unpublished
    end
    

    Then, you can move the logic to your post model, where it might look like this:

    scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
    scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
    
  • 11

    In Rails you find yourself looking at controllers, views and models for your database.

    To reduce the need for heavy configuration, Rails implements rules to ease up working with the application. You may define your own rules but for the beginning (and for later on) it's a good idea to stick to conventions that Rails offers.

    These conventions will speed up development, keep your code concise and readable and allow you an easy navigation inside your application.

    Conventions also lower the barriers to entry for beginners. There are so many conventions in Rails that a beginner doesn’t even need to know about, but can just benefit from in ignorance. It’s possible to create great applications without knowing why everything is the way it is.

    For Example

    If you have a database table called orders with the primary key id. The matching model is called order and the controller that handles all the logic is named orders_controller. The view is split in different actions: if the controller has a new and edit action, there is also a new and edit view.

    For Example

    To create an app you simply run rails new app_name. This will generate roughly 70 files and folders that comprise the infrastructure and foundation for your Rails app.

    It includes:

    • Folders to hold your models (database layer), controllers, and views
    • Folders to hold unit tests for your application
    • Folders to hold your web assets like Javascript and CSS files
    • Default files for HTTP 400 responses (i.e. file not found)
    • Many others
  • 8

    ActiveRecord includes default_scope, to automatically scope a model by default.

    class Post
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    

    The above code will serve posts which are already published when you perform any query on the model.

    Post.all # will only list published posts 
    

    That scope, while innocuous-looking, has multiple hidden side-effect that you may not want.

    default_scope and order

    Since you declared an order in the default_scope, calling order on Post will be added as additional orders instead of overriding the default.

    Post.order(updated_at: :desc)
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
    

    This is probably not the behavior you wanted; you can override this by excluding the order from the scope first

    Post.except(:order).order(updated_at: :desc)
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
    

    default_scope and model initialization

    As with any other ActiveRecord::Relation, default_scope will alter the default state of models initialized from it.

    In the above example, Post has where(published: true) set by default, and so new models from Post will also have it set.

    Post.new # => <Post published: true>
    

    unscoped

    default_scope can nominally be cleared by calling unscoped first, but this also has side-effects. Take, for example, an STI model:

    class Post < Document
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    

    By default, queries against Post will be scoped to type columns containing 'Post'. But unscoped will clear this along with your own default_scope, so if you use unscoped you have to remember to account for this as well.

    Post.unscoped.where(type: 'Post').order(updated_at: :desc)
    

    unscoped and Model Associations

    Consider a relationship between Post and User

    class Post < ApplicationRecord
      belongs_to :user
      default_scope ->{ where(published: true).order(created_at: :desc) }
    end
    
    class User < ApplicationRecord
      has_many :posts
    end
    

    By getting an individual User, you can see the posts related to it:

    user = User.find(1)
    user.posts
    
    SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]
    

    But you want to clear the default_scope from the posts relation, so you use unscoped

    user.posts.unscoped
    
    SELECT "posts".* FROM "posts"
    

    This wipes out the user_id condition as well as the default_scope.

    An example use-case for default_scope

    Despite all of that, there are situations where using default_scope is justifiable.

    Consider a multi-tenant system where multiple subdomains are served from the same application but with isolated data. One way to achieve this isolation is through default_scope. The downsides in other cases become upsides here.

    class ApplicationRecord < ActiveRecord::Base
      def self.inherited(subclass)
        super
    
        return unless subclass.superclass == self
        return unless subclass.column_names.include? 'tenant_id'
    
        subclass.class_eval do
          default_scope ->{ where(tenant_id: Tenant.current_id) }
        end
      end
    end
    

    All you need to do is set Tenant.current_id to something early in the request, and any table that contains tenant_id will automatically become scoped without any additional code. Instantiating records will automatically inherit the tenant id they were created under.

    The important thing about this use-case is that the scope is set once per request, and it doesn't change. The only cases you will need unscoped here are special cases like background workers that run outside of a request scope.

Please consider making a request to improve this example.

Syntax

Syntax

Parameters

Parameters

Remarks

Remarks

Still have a question about Rails Best Practices? Ask Question

Topic Outline