-
-
Save FND/1244491 to your computer and use it in GitHub Desktop.
Gemfile.lock |
#!/usr/bin/env ruby | |
# encoding: UTF-8 | |
# test case for eager loading of nested associations with DataMapper | |
require 'rubygems' | |
require 'dm-core' | |
require 'dm-constraints' | |
require 'dm-migrations' | |
require 'eager_loading' | |
DataMapper::Logger.new($stdout, :debug) | |
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/db.sqlite") | |
class Person | |
include DataMapper::Resource | |
property :id, Serial | |
property :name, String, :required => true | |
has n, :vehicles | |
end | |
class Vehicle | |
include DataMapper::Resource | |
property :id, Serial | |
property :name, String, :required => true | |
has n, :components | |
end | |
class Component | |
include DataMapper::Resource | |
property :id, Serial | |
property :name, String, :required => true | |
belongs_to :manufacturer | |
end | |
class Manufacturer | |
include DataMapper::Resource | |
property :id, Serial | |
property :name, String, :required => true | |
end | |
DataMapper.auto_migrate! | |
# generate test data | |
Person.create(:name => "FND", :vehicles => [ | |
Vehicle.create(:name => "Taurus", :components => [ | |
Component.create(:name => "engine", | |
:manufacturer => Manufacturer.create(:name => "Ford")), | |
Component.create(:name => "radio", | |
:manufacturer => Manufacturer.create(:name => "Bose")) | |
]), | |
Vehicle.create(:name => "fixie", :components => [ | |
Component.create(:name => "frame", | |
:manufacturer => Manufacturer.create(:name => "Campagnolo")), | |
Component.create(:name => "breaks", | |
:manufacturer => Manufacturer.create(:name => "Shimano")) | |
]) | |
]) | |
Person.create(:name => "tillsc", :vehicles => [ | |
Vehicle.create(:name => "Golf", :components => [ | |
Component.create(:name => "engine", | |
:manufacturer => Manufacturer.create(:name => "VW")) | |
]) | |
]) | |
# retrieve data | |
puts "", "[INFO] test case A" | |
person = Person.get!(1) | |
puts person.vehicles.components.manufacturer.map(&:name).join(", ") | |
puts "", "[INFO] test case B" | |
people = Person.all | |
people.each do |person| | |
person.vehicles.each do |vehicle| | |
puts sprintf("%-10s %-10s", person.name, vehicle.name) | |
end | |
end | |
puts "", "[INFO] test case C ===== /!\ n+1 hazard ====" | |
people = Person.all | |
people.each do |person| | |
person.vehicles.each do |vehicle| | |
vehicle.components.each do |component| | |
puts sprintf("%-10s %-10s %-10s", person.name, vehicle.name, component.name) | |
end | |
end | |
end | |
puts "", "[INFO] test case D" | |
people = Person.all | |
people.eager_load(Person.vehicles.components).each do |person| | |
person.vehicles.each do |vehicle| | |
vehicle.components.each do |component| | |
puts sprintf("%-10s %-10s %-10s", person.name, vehicle.name, component.name) | |
end | |
end | |
end |
# manual eager loading for DataMapper | |
# adapted from Chris Corbyn: https://gist.github.com/1244491#gistcomment-56797 | |
module EagerLoading | |
def eager_load(query_path) | |
scope = self | |
query_path.relationships.each do |relation| | |
source_key = relation.source_key.first # TODO: rename | |
target_key = relation.target_key.first # TODO: rename | |
# for each level in the query path, collect all the resources referencing | |
# keys at the current scope | |
next_scope = relation.target_model.all(target_key.name => scope. | |
collect(&:"#{source_key.name}")) | |
# map target keys to the resources that exist for them | |
links = next_scope.inject({}) do |map, resource| | |
map.merge(target_key.get(resource) => [resource]) { |k, v1, v2| v1 + v2 } | |
end | |
# now pre-load those from the map | |
scope.each do |parent| | |
if links.key?(source_key.get(parent)) | |
parent.instance_variable_set(:"@#{relation.name}", | |
links[source_key.get(parent)]) | |
end | |
end | |
# and step into the next nesting level | |
scope = next_scope | |
end | |
self | |
end | |
end | |
DataMapper::Collection.send(:include, EagerLoading) |
source :rubygems | |
DM_VERSION = '~> 1.2.0.rc2' | |
gem 'dm-core', DM_VERSION | |
gem 'dm-constraints', DM_VERSION | |
gem 'dm-migrations', DM_VERSION | |
gem 'dm-sqlite-adapter', DM_VERSION |
#!/usr/bin/env sh | |
rm db.sqlite | |
# reformat SQL queries for readability | |
bundle exec ./dm_el.rb | perl -pe 's#SELECT .*? (FROM ".*?" )(.*)#\1SELECT ... \2#' |
Hmm, I was just looking through some of Relationship
, and I noticed #eager_load
, which looks like exactly the API for this situation. So if I'm reading it correctly, you could simplify the whole thing to:
def load(query_path)
query_path.relationships.inject(@collection) do |scope, relationship|
relationship.eager_load(scope)
end
end
(BTW, I've vacillated, and I now prefer passing the Query::Path
to #load
instead of #initialize
(reverting to how you had it before). It seems more coherent, even though EagerLoader is probably still a single-use object.)
It's too late here for me to start hacking on this right now, but I'm going to try to make time to poke at it tomorrow.
@d11wtq—looks like Relationship#eager_load
does work; check out my fork of this gist: https://gist.github.com/1297105
Hey guys, I know this is an old thread, but check this out: https://gist.github.com/3100034
Awesome advice in
Relationship#set
! Will try that. Classic example of getting a bit lost :P (Excuse the fragmented response... I'm a bit distracted with food and another coding problem simultaneously ;))