Closer to Code

Tag: ActiveRecord::Relation (page 2 of 3)

ActiveRecord count vs length vs size and what will happen if you use it the way you shouldn’t

One of the most common and most deadly errors you can make: using length instead of count. You can repeat this multiple times, but you will always find someone who’ll use it the way it shouldn’t be used.

So, first just to make it clear:

#count – collection.count

  • Counts number of elements using SQL query (SELECT COUNT(*) FROM…)
  • #count result is not stored internally during object life cycle, which means, that each time we invoke this method, SQL query is performed again
  • count is really fast comparing to length
2.1.2 :048 > collection = User.all; nil
 => nil
2.1.2 :049 > collection.count
   (0.7ms)  SELECT COUNT(*) FROM `users`
 => 16053
2.1.2 :050 > collection.count
 => 16053

#length – collection.length

  • Returns length of a collecion without performing additional queries… as long as collection is loaded
  • When we have lazy loaded collection, length will load whole colletion into memory and then will return length of it
  • Might use all of your memory when used in a bad way
  • Really fast when having a eagerly loaded collection
2.1.2 :055 > collection = User.all; nil
 => nil
2.1.2 :056 > collection.length
  User Load (122.9ms)  SELECT `users`.* FROM `users`
 => 16053
2.1.2 :057 > collection = User.all; nil
 => nil
2.1.2 :058 > collection.to_a; nil
  User Load (140.9ms)  SELECT `users`.* FROM `users`
 => nil
2.1.2 :059 > collection.length
 => 16053
2.1.2 :060 > collection.length
 => 16053

#size – collection.size

  • Combines abilities of both previous methods;
  • If collection is loaded, will count it’s elements (no additional query)
  • If collection is not loaded, will perform additional query
2.1.2 :034 > collection = User.all; nil
 => nil 
2.1.2 :035 > collection.count
   (0.3ms)  SELECT COUNT(*) FROM `users`
 => 16053 
2.1.2 :036 > collection.count
   (0.3ms)  SELECT COUNT(*) FROM `users`
 => 16053 
2.1.2 :037 > collection.size
   (0.2ms)  SELECT COUNT(*) FROM `users`
 => 16053 
2.1.2 :038 > collection.to_a; nil
  User Load (64.2ms)  SELECT `users`.* FROM `users`
 => nil 
2.1.2 :039 > collection.size
 => 16053 

Why would you even care?

Well it might have a huge impact on your apps performance (and resource consumption). In general if you don’t want to care at all and you want to delegate this responsibility to someone else, use #size. If you want to care, then play with it and understand how it works, otherwise you might end up doing something like this:

print "We have #{User.all.length} users!"

And this is the performance difference on my computer (with only 16k users):

       user     system      total        real
count     0.010000   0.000000   0.010000 (  0.002989)
length    0.730000   0.060000   0.790000 (  0.846671)

Nearly 1 second to perform such simple task. And this could have a serious impact on your web app! Keep that in mind.

ActiveResource relations – a bit of magic to make it look and feel more like ActiveModel relations

ActiveResource collection new problem

ActiveResource can be pretty helpful when you have a RESTful JSON API. Although it has some limitations. One of the most irritating is a lack of nested resources new scope method. When you have structure like this:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats
end

class DailyStat  < ActiveResource::Base
  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

You can do some basic stuff:

User.last #=> User instance
User.all #=> [User, User]
user = User.last
user.daily_stats #=> [DailyStat, DailyStat]

But unfortunately if you try something like this:

user = User.last
stat = user.daily_stats.new
stat.save!

You’ll get following error:

user.daily_stats.new
NoMethodError: undefined method `new' for #<ActiveResource::Collection:0x000000080d1a30>

Of course we can do this the other way around:

user = User.last
stats = DailyStat.new(user_id: user.id)
stats.save!

But it just doesn’t seem right (well at least after working with ActiveRecord). ActiveResource::Collection doesn’t support building resources through it.

alias_method and a bit of magic as a solution

To obtain such a behaviour we have to:

  • save original relation method using alias_method
  • create a module that will contain our extension that will allow us to build new resources directly
  • define relation method that will mix the module with original relation method output
  • return mixed relation output

Once we have all of this, we will be able to just:

user = User.last
new_stat = user.daily_stats.new
new_stat.save!

overwriting method without losing the original one

Ok, so we have our relation daily_stats method that will return us a given ActiveResource::Collection. We will have to overwrite it, but we can’t lose the original one. To obtain this, we can use alias_method:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :native_daily_stats, :daily_stats

  def daily_stats
    # Now we can do whatever we want here, because we can get
    # the ActiveResource::Collection of DailyStat via
    # native_daily_stats method
    # Something fancy will happen here...
    # and after that we will just return native_daily_stats
    native_daily_stats
  end
end

It’s worth pointing out, that you can use this trick to redefine/change method without losing the original one. Especially when you can’t use super (because it’s not an inherited one, etc).

Extension module for our new daily_stats

Now we have to create our extension that will be used to modify the ActiveResource::Collection (but only in a daily_stats scope):

class DailyStat < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  # Here should go previously declarated DailyStat class code...
end

Hooking it all togethers

Finally, we can join all previously created elements in a new daily_stats method:

class User < ActiveResource::Base
  # User code...  

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

Now you can create resources that belong to other, directly via their scope.

TLl;DR version

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :original_daily_stats, :daily_stats

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

class DailyStat  < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      # Here magic happens - original params contain relation details (user_id: user.id)
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end
Olderposts Newerposts

Copyright © 2021 Closer to Code

Theme by Anders NorenUp ↑