What's new in Active Record [Rails 4 Countdown to 2013]

Posted on

This post is part of a series of 31 Rails 4 articles being released each day in December 2012.

In today's Rails 4 countdown post, we are going to go over some of the changes being made to Active Record. While this list does not include every single change, it does summarize most of the non-specific database vendor changes.

Null Object Pattern

Being introduced in Rails 4 is ActiveRecord::QueryMethods.none, which implements the null object pattern. It is to be used in instances where you have a method which returns a relation, but there is a condition in which you do not want the database to be queried. All subsequent chained conditions will work without issue, eliminating the need to continuously check if the object your are working with is a relation.

Team.none.where(name: 'Justice League')
# => #<ActiveRecord::Relation []>

Scopes require a callable object

Scopes have always been an area of frustration for new Rails developers. The biggest culprit of issues has to do with the eager-evaluation of scopes defined without a lambda. An example of this is time based conditions being eagerly-evaluated:

scope :recent, where(published_at: Time.now - 2.weeks)

Any future call to the recent scope, would always be 2 weeks before its initial evaluation.

In Rails 4, all scopes must use a callable object such as a Proc or lambda:

class Team < ActiveRecord::Base
  scope :active, -> { where(status: 'active') }
end

IdentityMap Removed

Since the IdentityMap had some inconsistencies with associations which were never resolved, it has been removed from Rails 4. It may return one day if the issues are fixed. If you are upgrading a Rails 3 application, please remove config.active_record.identity_map = true from your config/application.rb file.

destroy!

Active Record now has a #destroy! method to compliment #create!. If a record cannot be destroyed, an ActiveRecord::RecordNotDestroyed exception will be raised.

update_columns

Active Record now has two new methods, #update_columns and #update_column. By calling either of these methods, the attribute(s) passed-in will update the fields in the database, bypassing validations and callbacks.

@team.update_column(:name, 'X-Men')
@team.update_columns(name: 'X-Men', universe: 'Marvel')

Model.all

No longer will a call to Model.all execute a query immediately and return an array of records. In Rails 4, calls to Model.all is equivalent to now deprecated Model.scoped. This means that more relations can be chained to Model.all and the result will be lazily evaluated.

Default ordering added to first

By calling first in previous versions of Rails, a simple LIMIT 1 would be added to the generated SQL query. This does not guarantee the record with the lowest primary key is returned.

Calling first in Rails 4 will add an implicit order to the SQL query. This ensures the record with the lowest primary key is returned first.

Team.first
Rails 3: SELECT "teams".* FROM "teams" LIMIT 1
Rails 4: SELECT "teams".* FROM "teams" ORDER BY "teams"."id" ASC LIMIT 1

Deprecated Finders

In previous versions of Rails, Active Record provided a finder method for every field defined in your table. For example, if you had an email field on a User model, you could execute: User.find_by_email('some@emailaddress.com') or User.find_all_by_email('some@emailaddress.com'). On top of this, you could chain together fields by including an and between the fields.

The Rails 2 style dynamic finder methods are finally being deprecated as of Rails 4. The code has been extracted to a gem named activerecord-deprecated_finders. The gem will be a dependency in Rails 4.0, but will be removed as of Rails 4.1.

Here are examples of some deprecated finders and their Rails 4 implementations:

# find_all_by_
Rails 3: Team.find_all_by_name('Justice League')
Rails 4: Team.where(name: 'Justice League')

# find_last_by_
Rails 3: Team.find_last_by_name('Justice League')
Rails 4: Team.where(name: 'Justice League').last

# find_or_create_by_
Rails 3: Team.find_or_create_by_name('Justice League')
Rails 4: Team.where(name: 'Justice League').first_or_create

# find_or_create_by_...!
Rails 3: Team.find_or_create_by_name!('Justice League')
Rails 4: Team.where(name: 'Justice League').first_or_create!

# find_or_initialize_by_
Rails 3: Team.find_or_initialize_by_name('Justice League')
Rails 4: Team.where(name: 'Justice League').first_or_initialize

# scoped_by_
Rails 3: Team.scoped_by_name('Justice League')
Rails 4: Team.where(name: 'Justice League')

The old hash based finder API has also been deprecated. If you have been using the new style introduced in Rails 3, this should not affect your codebase. This syntax is also included in the activerecord-deprecated_finders gem.

Here is an example of how to convert the old hash finder to use ActiveRecord::QueryMethods instead:

Rails 3: Team.find(:all, conditions: { name: 'X-Men' })
Rails 4: Team.where(name: 'X-Men')

find_by and find_by!

To mirror the functionality removed from the dynamic finders, methods #find_by and #find_by! have been added to Rails 4:

Member.find_by name: 'Batman', city: 'Gotham'
Member.find_by! name: 'Superman'

Multiple columns with Pluck

The relation method #pluck allows you to select a single column and return the results in an array with the specified values instead of Active Record instances.

As of Rails 4, you will be able to specify multiple columns using #pluck:

Member.pluck(:name, :bio)
# >> [["Superman", "From Krypton"], ["Batman", "From Gotham City"]]

References

The .includes query method is meant for the eager loading of associations. Some developers use the .includes method to perform some LEFT OUTER JOIN queries on associations using string SQL snippets. As of Rails 4, adding a string condition of the included association results in a deprecation warning.

Here is an example that selects all Teams which have a member named Batman:

Team.includes(:members).where('members.name = ?', 'Batman')

# Rails 3
SQL (0.7ms)  SELECT "teams"."id" AS t0_r0, "teams"."name" AS t0_r1, "teams"."created_at" AS t0_r2, "teams"."updated_at" AS t0_r3, "members"."id" AS t1_r0, "members"."name" AS t1_r1, "members"."bio" AS t1_r2, "members"."team_id" AS t1_r3, "members"."created_at" AS t1_r4, "members"."updated_at" AS t1_r5 FROM "teams" LEFT OUTER JOIN "members" ON "members"."team_id" = "teams"."id" WHERE (members.name = 'Batman')

# Rails 4
DEPRECATION WARNING: It looks like you are eager loading table(s) (one of: teams, members) that are referenced in a string SQL snippet.
...

While the query still executes in Rails 4, it is recommended to include the new query method ActiveRecord::QueryMethods.references instead:

Team.includes(:members).where("members.name = ?", 'Batman').references(:members)

However, if you were using the hash syntax with association conditions, it will still perform a LEFT OUTER JOIN without any deprecation warnings:

Team.includes(:members).where(members: { name:  'Batman' })

Also, ordering string SQL snippets on included associations will still work the same way without the need of references:

Team.includes(:members).order('members.name')

Further Reading

Be on the lookout for a future countdown post that goes into detail about some vendor specific Active Record changes for PostgreSQL.

002

This post is by Kevin Faustino. Kevin is the Chief Craftsman of Remarkable Labs and also the founder of the Toronto Ruby Brigade.


Comments

comments powered by Disqus