Rails 4.1 compatibility

Why bother?

  • No features in 3.x branch since February 2013
  • No bugfixes since late 2013 (?)
  • Rails 4 is threadsafe by default
  • Simple tests show that Rails 4 is notably faster than 3

Cost of the change

Many things has been deprecated or removed (preceded by earlier deprecation notices that we happily ignored). Code working in Rails 3 not necessarily works in Rails 4 without changes.

Deprecated finders

DON'T

User.find(:all, 
    :conditions => {:name => 'Jane Doe'}, 
    :limit => 3, 
    :order => :created_at)

DO

User.where(name: 'Jane Doe').limit(3).order(created_at: :desc)

Last resort

Gem: activerecord-deprecated_finders

 

https://github.com/rails/activerecord-deprecated_finders

 

But we are trying to avoid it.

Behaviour of "includes"

Rails 3

[3] pry(main)> Order.includes(:owner_subject).limit(3)
  Order Load (0.9ms)  SELECT "orders".* FROM "orders" LIMIT 3
  Subject Load (59.2ms)  SELECT "subjects".* FROM "subjects" WHERE "subjects"."id" IN (13)

[5] pry(main)> Order.includes(:owner_subject).limit(3).where('subjects.id > 1')
  SQL (2.0ms)  SELECT "orders"."id" AS t0_r0, "orders"."created_at" AS t0_r1, 
"orders"."type" AS t0_r2, "orders"."name" AS t0_r3, "orders"."number" AS t0_r4, 
"orders"."job_process_id" AS t0_r5, "orders"."root_id" AS t0_r6, 
"orders"."parent_id" AS t0_r7, "orders"."lft" AS t0_r8, "orders"."rgt" AS t0_r9, 
"orders"."owner_subject_id" AS t0_r10, "orders"."status" AS t0_r11, 
"orders"."deleted" AS t0_r12, "orders"."mis_ref" AS t0_r13, 
"orders"."altered_at" AS t0_r14, "orders"."version_no" AS t0_r15, 
"orders"."current_version_no" AS t0_r16, "orders"."seq_no" AS t0_r17, 
"subjects"."id" AS t1_r0, "subjects"."kind" AS t1_r1, "subjects"."parent_id" AS t1_r2, 
"subjects"."lft" AS t1_r3, "subjects"."rgt" AS t1_r4, "subjects"."domain_id" AS t1_r5, 
"subjects"."language_locale" AS t1_r6 FROM "orders" LEFT OUTER JOIN "subjects" 
ON "subjects"."id" = "orders"."owner_subject_id" WHERE (subjects.id > 1) LIMIT 3

Rails 4

2.1.2 :007 >   Document.includes(:user).where('id > 2')
  Document Load (1.0ms)  SELECT "documents".* FROM "documents" WHERE (id > 2)
  User Load (1.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (3, 1)


2.1.2 :008 > Document.includes(:user).where('users.id < 2')
  Document Load (1.5ms)  SELECT "documents".* FROM "documents" WHERE (users.id < 2)
PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "users"
LINE 1: SELECT "documents".* FROM "documents" WHERE (users.id < 2)
                                                     ^
: SELECT "documents".* FROM "documents" WHERE (users.id < 2)
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing 
    FROM-clause entry for table "users"
LINE 1: SELECT "documents".* FROM "documents" WHERE (users.id < 2)

N+1 queries

It is not sufficient to change all "includes" to "joins"!

Only "joins"

2.1.2 :016 > Document.joins(:files).each {|d| d.files.map(&:id) }
  Document Load (1.9ms)  SELECT "documents".* FROM "documents" INNER JOIN "document_files" ON "document_files"."document_id" = "documents"."id"
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 1]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 2]]
  DocumentFile Load (0.6ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 3]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 4]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 5]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 6]]
  DocumentFile Load (0.4ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 7]]
  DocumentFile Load (0.4ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 8]]
  DocumentFile Load (0.4ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 9]]
  DocumentFile Load (0.3ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 10]]
  DocumentFile Load (0.3ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 11]]
  DocumentFile Load (0.3ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 12]]
  DocumentFile Load (0.4ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 11]]
  DocumentFile Load (0.4ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 10]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 1]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 13]]
  DocumentFile Load (0.5ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 13]]
  DocumentFile Load (0.6ms)  SELECT "document_files".* FROM "document_files" WHERE "document_files"."document_id" = $1  [["document_id", 14]]
[...]

Combined with "includes"

2.1.2 :017 > Document.joins(:files).includes(:files).each {|d| d.files.map(&:id) }
  SQL (2.6ms)  SELECT "documents"."id" AS t0_r0, "documents"."name" AS t0_r1, 
"documents"."status" AS t0_r2, "documents"."user_id" AS t0_r3, "documents"."created_at" AS t0_r4, 
"documents"."updated_at" AS t0_r5, "documents"."file_file_name" AS t0_r6, 
"documents"."file_content_type" AS t0_r7, "documents"."file_file_size" AS t0_r8, 
"documents"."file_updated_at" AS t0_r9, "documents"."details" AS t0_r10, 
"document_files"."id" AS t1_r0, "document_files"."document_id" AS t1_r1, 
"document_files"."intent" AS t1_r2, "document_files"."created_at" AS t1_r3, 
"document_files"."updated_at" AS t1_r4, "document_files"."file_file_name" AS t1_r5, 
"document_files"."file_content_type" AS t1_r6, "document_files"."file_file_size" AS t1_r7, 
"document_files"."file_updated_at" AS t1_r8, "document_files"."file_processing" AS t1_r9 
FROM "documents" INNER JOIN "document_files" ON "document_files"."document_id" = "documents"."id"

Routing

In Rails 4 routing with "match" without specifying verbs is illegal.

# DON'T

match 'orders/:id/condemn' => 'orders#condemn'


# DO

match 'orders/:id/condemn' => 'orders#condemn', via: [:post]


# DO BETTER

post 'orders/:id/condemn' => 'orders#condemn'

Double route-names are forbidden


# DON'T

post '' => 'orders#create', :as => :orders
match '' => 'orders#index', :as => :orders

Scope must be a callable object (lambda)

Scope


# DON'T

scope :not_deleted, where(deleted: false)
has_many :good_comments, conditions: 'karma > 1'

# DO

scope :not_deleted, -> { where(deleted: false) }
has_many :good_comments, -> { where('karma > 1') }

No need to serialize to JSON

Rails 4 has native support for JSON and hstore types in PotgreSQL. Calling "serialize" causes double serialization, which is never good.


# DON'T

serialize :options, ActiveRecord::Coders::Hstore


# DO

# well, just make sure that column is of hstore type and you're good to go

Caveat

# DON'T

@model.options[:active] = true
@model.save!


# DO

@model.options_will_change!
# or @model.attribute_will_change!(:options)
@model.options[:active] = true
@model.save!

# OR DO

opts = @model.options
opts[:active] = true
@model.options = opts

Strong parameters

Strong parameters

  • attr_accessible and attr_protected are removed
  • allowed attributes are defined in controller

def safe_params
	params.require(:document).permit(files_attributes: [:file])
end

Other changes

PATCH instead of PUT

PATCH is default verb for update action since Rails 4.0, however PUT is still supported.

No "assets" group in Gemfile

vendor directory is no longer supported

deck

By katafrakt

deck

  • 974