Skip to content

Commit

Permalink
Add association_join to model datasets, for setting up joins based on…
Browse files Browse the repository at this point in the history
… associations

There have been many requests for this type of functionality in the
past, but I could never think of a good name for it.  I've given up
on a good name, so association_join it is.

Implementation is fairly simple, piggy-backing on the eager_graph
support, and just copying the resulting JOIN.  Only a couple new
features were needed:

1) Making the default eager_grapher procs respect a passed in
:eager_graph :join_type option and override the one used for the
association.

2) Making Dataset#graph not use a from_self in cases, as otherwise
using separate association_join calls doesn't work (since the
:graph/:eager_graph metadata is retained across calls).
  • Loading branch information
jeremyevans committed Feb 6, 2014
1 parent 21bfe7f commit 8dbec2f
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== HEAD

* Add association_join to model datasets, for setting up joins based on associations (jeremyevans)

* Add one_through_many association to many_through_many plugin, for only returning a single record (jeremyevans)

* Add :graph_order association option, useful when :order needs to contain qualified identifiers (jeremyevans)
Expand Down
22 changes: 22 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,28 @@ You can dynamically customize eager loads for both +eager+ and +eager_graph+ whi
# Eagerly load only replies containing 'foo', and the person and tags for those replies
Post.eager(:replies=>{proc{|ds| ds.where(Sequel.like(text, '%foo%'))}=>[:person, :tags]}).all

=== Joining with Associations

You can use the association_join method to add a join to the model's dataset based on the assocation:

Post.association_join(:author)
# SELECT * FROM posts
# INNER JOIN authors AS author ON (author.id = posts.author_id)

This comes with variants for different join types:

Post.association_left_join(:replies)
# SELECT * FROM posts
# LEFT JOIN replies ON (replies.post_id = posts.id)

Similar to the eager loading methods, you can use multiple associations and nested associations:

Post.association_join(:author, :replies=>:person).all
# SELECT * FROM posts
# INNER JOIN authors AS author ON (author.id = posts.author_id)
# INNER JOIN replies ON (replies.post_id = posts.id)
# INNER JOIN people AS person ON (person.id = replies.person_id)

=== Extending the underlying dataset

The recommended way to implement table-wide logic by defining methods on the dataset using +dataset_module+:
Expand Down
6 changes: 6 additions & 0 deletions doc/association_basics.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ As is the code to add a related album to an artist:

@artist.add_album(:name=>'RF')

It also makes it easier to creating queries that use joins based on the association:

Artist.association_join(:albums)
# SELECT * FROM artists
# INNER JOIN albums ON (albums.artist_id = artists.id)

== The Types of Associations

Sequel has five different association types built in:
Expand Down
4 changes: 2 additions & 2 deletions lib/sequel/dataset/graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def graph(dataset, join_conditions = nil, options = OPTS, &block)
# Only allow table aliases that haven't been used
raise_alias_error.call if @opts[:graph] && @opts[:graph][:table_aliases] && @opts[:graph][:table_aliases].include?(table_alias)

# Use a from_self if this is already a joined table
ds = (!@opts[:graph] && (@opts[:from].length > 1 || @opts[:join])) ? from_self(:alias=>options[:from_self_alias] || first_source) : self
# Use a from_self if this is already a joined table (or from_self specifically disabled for graphs)
ds = (@opts[:graph_from_self] != false && !@opts[:graph] && (@opts[:from].length > 1 || @opts[:join])) ? from_self(:alias=>options[:from_self_alias] || first_source) : self

# Join the table early in order to avoid cloning the dataset twice
ds = ds.join_table(options[:join_type] || :left_outer, table, join_conditions, :table_alias=>table_alias, :implicit_qualifier=>options[:implicit_qualifier], :qualify=>options[:qualify], &block)
Expand Down
57 changes: 44 additions & 13 deletions lib/sequel/model/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1263,7 +1263,7 @@ def one_to_one(name, opts=OPTS, &block)
end

Plugins.inherited_instance_variables(self, :@association_reflections=>:dup, :@autoreloading_associations=>:hash_dup, :@default_eager_limit_strategy=>nil)
Plugins.def_dataset_methods(self, [:eager, :eager_graph])
Plugins.def_dataset_methods(self, [:eager, :eager_graph, :association_join, :association_full_join, :association_inner_join, :association_left_join, :association_right_join])

private

Expand Down Expand Up @@ -1378,8 +1378,8 @@ def def_many_to_many(opts)
jt_graph_block = opts[:graph_join_table_block]
opts[:eager_grapher] ||= proc do |eo|
ds = eo[:self]
ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block)
ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>ds.opts[:eager_graph][:join_type]||jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, &graph_block)
end

def_association_dataset_methods(opts)
Expand Down Expand Up @@ -1465,7 +1465,7 @@ def def_many_to_one(opts)
graph_cks = opts[:graph_keys]
opts[:eager_grapher] ||= proc do |eo|
ds = eo[:self]
ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
end

def_association_dataset_methods(opts)
Expand Down Expand Up @@ -1539,7 +1539,7 @@ def def_one_to_many(opts)
graph_block = opts[:graph_block]
opts[:eager_grapher] ||= proc do |eo|
ds = eo[:self]
ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
# We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal
ds
Expand Down Expand Up @@ -1961,6 +1961,26 @@ def set_one_to_one_associated_object(opts, o)
# Artist.eager(:albums => {proc{|ds| ds.where{year > 1990}}=>{:tracks => :genre}})
module DatasetMethods
Sequel::Dataset.def_mutation_method(:eager, :eager_graph, :module=>self)

%w'inner left right full'.each do |type|
class_eval <<END, __FILE__, __LINE__+1
def association_#{type}_join(*associations)
_association_join(:#{type}, associations)
end
END
end

# Adds one or more INNER JOINs to the existing dataset using the keys and conditions
# specified by the given association. The following methods also exist for specifying
# a different type of JOIN:
#
# association_full_join :: FULL JOIN
# association_inner_join :: INNER JOIN
# association_left_join :: LEFT JOIN
# association_right_join :: RIGHT JOIN
def association_join(*associations)
association_inner_join(*associations)
end

# If the expression is in the form <tt>x = y</tt> where +y+ is a <tt>Sequel::Model</tt>
# instance, array of <tt>Sequel::Model</tt> instances, or a <tt>Sequel::Model</tt> dataset,
Expand Down Expand Up @@ -2082,16 +2102,10 @@ def eager_graph(*associations)
ds = clone(:eager_graph=>eg)
ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations)
else
# Each of the following have a symbol key for the table alias, with the following values:
# :reciprocals - the reciprocal instance variable to use for this association
# :reflections - AssociationReflection instance related to this association
# :requirements - array of requirements for this association
ds = clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0, :row_proc=>row_proc})
ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations).
naked
new_eager_graph(associations)
end
end

# Do not attempt to split the result set into associations,
# just return results as simple objects. This is useful if you
# want to use eager_graph as a shortcut to have all of the joins
Expand Down Expand Up @@ -2182,8 +2196,25 @@ def eager_graph_build_associations(hashes)
hashes.replace(EagerGraphLoader.new(self).load(hashes))
end

# Setup a new eager graph for the given associations and options
def new_eager_graph(associations, opts=OPTS)
# Each of the following have a symbol key for the table alias, with the following values:
# :reciprocals - the reciprocal instance variable to use for this association
# :reflections - AssociationReflection instance related to this association
# :requirements - array of requirements for this association
opts = {:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0, :row_proc=>row_proc}.merge(opts)
ds = clone(:eager_graph=>opts)
ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations).naked
end

private

# Return a new dataset with JOINs of the given type added, using the tables and
# conditions specified by the associations.
def _association_join(type, associations)
clone(:join=>clone(:graph_from_self=>false).new_eager_graph(associations, :join_type=>type).opts[:join])
end

# If the association has conditions itself, then it requires additional filters be
# added to the current dataset to ensure that the passed in object would also be
# included by the association's conditions.
Expand Down
4 changes: 2 additions & 2 deletions lib/sequel/plugins/many_through_many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,11 @@ def def_many_through_many(opts)
ds = eo[:self]
iq = eo[:implicit_qualifier]
opts.edges.each do |t|
ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, &t[:block])
ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>ds.opts[:eager_graph][:join_type]||t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, &t[:block])
iq = nil
end
fe = opts.final_edge
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block)
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, &graph_block)
end

def_association_dataset_methods(opts)
Expand Down
4 changes: 2 additions & 2 deletions lib/sequel/plugins/pg_array_associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def def_many_to_pg_array(opts)

opts[:eager_grapher] ||= proc do |eo|
ds = eo[:self]
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds
end

Expand Down Expand Up @@ -404,7 +404,7 @@ def def_pg_array_to_many(opts)

opts[:eager_grapher] ||= proc do |eo|
ds = eo[:self]
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>ds.opts[:eager_graph][:join_type]||join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
ds
end

Expand Down
8 changes: 8 additions & 0 deletions spec/extensions/many_through_many_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,10 @@ def columns
proc{@c1.eager_graph(Object.new)}.should raise_error(Sequel::Error)
end

it "should support association_join" do
@c1.association_join(:tags).sql.should == "SELECT * FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags ON (tags.id = albums_tags.tag_id)"
end

it "should eagerly graph a single many_through_many association" do
a = @c1.eager_graph(:tags).all
a.should == [@c1.load(:id=>1)]
Expand Down Expand Up @@ -1762,6 +1766,10 @@ def columns
DB.sqls.length.should == 0
end

it "should support association_join" do
@c1.association_join(:tag).sql.should == "SELECT * FROM artists INNER JOIN albums_artists ON (albums_artists.artist_id = artists.id) INNER JOIN albums ON (albums.id = albums_artists.album_id) INNER JOIN albums_tags ON (albums_tags.album_id = albums.id) INNER JOIN tags AS tag ON (tag.id = albums_tags.tag_id)"
end

it "should eagerly graph a single one_through_many association" do
a = @c1.eager_graph(:tag).all
a.should == [@c1.load(:id=>1)]
Expand Down
5 changes: 5 additions & 0 deletions spec/extensions/pg_array_associations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,11 @@ def id3
DB.sqls.should == []
end

it "should support association_join" do
@c1.association_join(:tags).sql.should == "SELECT * FROM artists INNER JOIN tags ON (artists.tag_ids @> ARRAY[tags.id])"
@c2.association_join(:artists).sql.should == "SELECT * FROM tags INNER JOIN artists ON (artists.tag_ids @> ARRAY[tags.id])"
end

it "should eagerly graph associations" do
@c2.dataset._fetch = {:id=>2, :artists_id=>1, :tag_ids=>Sequel.pg_array([1,2,3])}
@c1.dataset._fetch = {:id=>1, :tags_id=>2, :tag_ids=>Sequel.pg_array([1,2,3])}
Expand Down
Loading

0 comments on commit 8dbec2f

Please sign in to comment.