Search code examples
rubyassociationseager-loadingsequel

Sequel gem: Why does eager().all work but eager().first doesn't?


I'm storing a user's profile fields in a separate table, and want to look up a user by email address (for password reset). Trying to determine the best approach, and ran into this unexpected behaviour inconsistency.

Schema

create_table(:users) do
  String        :username,                  primary_key: true
  ...
end

create_table(:user_fields) do
  primary_key   :id
  foreign_key   :user_id, :users, type: String, null: false
  String        :label, null: false
  String        :value, null: false
end

Console Session

This version works (look up field, eager load it's associated user, call .all, take the first one):

irb(main):005:0> a = UserField.where(label: 'email', value: 'testuser@test.com').eager(:user).all[0]
I, [2015-09-29T17:54:06.273263 #147]  INFO -- : (0.000176s) SELECT * FROM `user_fields` WHERE ((`label` = 'email') AND (`value` = 'testuser@test.com'))
I, [2015-09-29T17:54:06.273555 #147]  INFO -- : (0.000109s) SELECT * FROM `users` WHERE (`users`.`username` IN ('testuser'))
=> #<UserField @values={:id=>2, :user_id=>"testuser", :label=>"email", :value=>"testuser@test.com"}>
irb(main):006:0> a.user
=> #<User @values={:username=>"testuser"}>

You can see both queries (field and user) are kicked off together, and when you try to access a.user, the data's already loaded.

But when I try calling .first in place of .all:

irb(main):007:0> b = UserField.where(label: 'email', value: 'testuser@test.com').eager(:user).first
I, [2015-09-29T17:54:25.832064 #147]  INFO -- : (0.000197s) SELECT * FROM `user_fields` WHERE ((`label` = 'email') AND (`value` = 'testuser@test.com')) LIMIT 1
=> #<UserField @values={:id=>2, :user_id=>"testuser", :label=>"email", :value=>"testuser@test.com"}>
irb(main):008:0> b.user
I, [2015-09-29T17:54:27.887718 #147]  INFO -- : (0.000172s) SELECT * FROM `users` WHERE (`username` = 'testuser') LIMIT 1
=> #<User @values={:username=>"testuser"}>

The eager load fails -- it doesn't kick off the second query for the user object until you try to reference it with b.user.

What am I failing to understand about the sequel gem API here? And what's the best way to load a model instance based on the attributes of it's associated models? (find user by email address)


Solution

  • Eager loading only makes sense when loading multiple objects. And in order to eager load, you need all of the current objects first, in order to get all associated objects in one query. With each, you don't have access to all current objects first, since you are iterating over them.

    You can use the eager_each plugin if you want Sequel to handle things internally for you, though note that it makes dataset.first do something similar to dataset.all.first for eagerly loaded datasets. But it's better to not eager load if you only need one object, and to call all if you need to eagerly load multiple ones.