acts-as-joinable 0.0.6 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +3 -1
- data/Rakefile +1 -1
- data/lib/acts-as-joinable.rb +3 -0
- data/lib/acts_as_joinable/active_record_patch.rb +60 -0
- data/lib/acts_as_joinable/core.rb +23 -31
- data/test/test_acts_as_joinable.rb +20 -1
- data/test/test_helper.rb +13 -1
- metadata +4 -4
data/README.markdown
CHANGED
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
|
|
5
5
|
spec = Gem::Specification.new do |s|
|
6
6
|
s.name = "acts-as-joinable"
|
7
7
|
s.authors = ["Lance Pollard"]
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.1.0"
|
9
9
|
s.summary = "ActsAsJoinable: DRYing up Many-to-Many Relationships in ActiveRecord"
|
10
10
|
s.homepage = "http://github.com/viatropos/acts-as-joinable"
|
11
11
|
s.email = "lancejpollard@gmail.com"
|
data/lib/acts-as-joinable.rb
CHANGED
@@ -70,6 +70,9 @@ module ActsAsJoinable
|
|
70
70
|
def acts_as_relationship
|
71
71
|
belongs_to :parent, :polymorphic => true
|
72
72
|
belongs_to :child, :polymorphic => true
|
73
|
+
|
74
|
+
# validates_uniqueness_of :parent_id, :scope => [:parent_type, :child_id]
|
75
|
+
# validates_uniqueness_of :child_id, :scope => [:child_type, :parent_id]
|
73
76
|
|
74
77
|
# ActsAsJoinable.models.each do |m|
|
75
78
|
# belongs_to "parent_#{m}".intern, :foreign_key => 'parent_id', :class_name => m.camelize
|
@@ -1,4 +1,64 @@
|
|
1
1
|
module ActiveRecord
|
2
|
+
module AssociationPreload
|
3
|
+
module ClassMethods
|
4
|
+
def find_associated_records(ids, reflection, preload_options)
|
5
|
+
options = reflection.options
|
6
|
+
table_name = reflection.klass.quoted_table_name
|
7
|
+
|
8
|
+
if interface = reflection.options[:as]
|
9
|
+
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} IN ('#{self.name}','#{self.base_class.sti_name}')"
|
10
|
+
else
|
11
|
+
foreign_key = reflection.primary_key_name
|
12
|
+
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
conditions << append_conditions(reflection, preload_options)
|
16
|
+
|
17
|
+
reflection.klass.with_exclusive_scope do
|
18
|
+
reflection.klass.find(:all,
|
19
|
+
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
20
|
+
:include => preload_options[:include] || options[:include],
|
21
|
+
:conditions => [conditions, ids],
|
22
|
+
:joins => options[:joins],
|
23
|
+
:group => preload_options[:group] || options[:group],
|
24
|
+
:order => preload_options[:order] || options[:order])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def preload_through_records(records, reflection, through_association)
|
29
|
+
through_reflection = reflections[through_association]
|
30
|
+
through_primary_key = through_reflection.primary_key_name
|
31
|
+
|
32
|
+
if reflection.options[:source_type]
|
33
|
+
interface = reflection.source_reflection.options[:foreign_type]
|
34
|
+
preload_options = {:conditions => ["#{connection.quote_column_name interface} IN (?)", reflection.options[:source_type]]}
|
35
|
+
|
36
|
+
records.compact!
|
37
|
+
records.first.class.preload_associations(records, through_association, preload_options)
|
38
|
+
|
39
|
+
# Dont cache the association - we would only be caching a subset
|
40
|
+
through_records = []
|
41
|
+
records.each do |record|
|
42
|
+
proxy = record.send(through_association)
|
43
|
+
|
44
|
+
if proxy.respond_to?(:target)
|
45
|
+
through_records << proxy.target
|
46
|
+
proxy.reset
|
47
|
+
else # this is a has_one :through reflection
|
48
|
+
through_records << proxy if proxy
|
49
|
+
end
|
50
|
+
end
|
51
|
+
through_records.flatten!
|
52
|
+
else
|
53
|
+
records.first.class.preload_associations(records, through_association)
|
54
|
+
through_records = records.map {|record| record.send(through_association)}.flatten
|
55
|
+
end
|
56
|
+
through_records.compact!
|
57
|
+
through_records
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
2
62
|
module Associations
|
3
63
|
class HasManyThroughAssociation < HasManyAssociation
|
4
64
|
protected
|
@@ -74,8 +74,10 @@ module ActsAsJoinable
|
|
74
74
|
|
75
75
|
# parent, child, or contexts (both) for custom helper getters/setters
|
76
76
|
|
77
|
-
has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :
|
78
|
-
has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :
|
77
|
+
has_many :parent_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :child, :foreign_key => "child_id", :uniq => true
|
78
|
+
has_many :child_relationships, :class_name => 'ActsAsJoinable::Relationship', :as => :parent, :foreign_key => "parent_id", :uniq => true
|
79
|
+
|
80
|
+
after_destroy :destroy_relationships unless after_destroy.map(&:method).include?(:destroy_relationships)
|
79
81
|
|
80
82
|
related_classes = (ancestors.reverse - included_modules + send(:subclasses)).uniq
|
81
83
|
wanted_classes = []
|
@@ -103,20 +105,6 @@ module ActsAsJoinable
|
|
103
105
|
:conditions => sql
|
104
106
|
}
|
105
107
|
|
106
|
-
has_both_relationships = relationships.length > 1
|
107
|
-
|
108
|
-
# don't know how to do this with has_many, since the join model requires 2
|
109
|
-
# different polymorphic models, and it breaks if they're the same class
|
110
|
-
if has_both_relationships
|
111
|
-
# you can only get all objects by this, method.
|
112
|
-
# to create them, use `child_(posts)`, `parent_(posts)`, etc.
|
113
|
-
# define_method type do
|
114
|
-
# [:parent, :child].map do |relationship|
|
115
|
-
# send("#{relationship.to_s}_#{type.to_s}") if respond_to?("#{relationship.to_s}_#{type.to_s}")
|
116
|
-
# end.flatten.compact.uniq
|
117
|
-
# end
|
118
|
-
end
|
119
|
-
|
120
108
|
# relationships == [:parent, :child]
|
121
109
|
relationships.each do |relationship|
|
122
110
|
relationship = opposite_for(relationship)
|
@@ -151,8 +139,7 @@ module ActsAsJoinable
|
|
151
139
|
through_options = {
|
152
140
|
:class_name => "ActsAsJoinable::Relationship",
|
153
141
|
:conditions => conditions,
|
154
|
-
:as => opposite_for(relationship).to_sym
|
155
|
-
:dependent => :destroy
|
142
|
+
:as => opposite_for(relationship).to_sym
|
156
143
|
}
|
157
144
|
|
158
145
|
through_options[:uniq] = true unless association_type == :has_one
|
@@ -172,11 +159,9 @@ module ActsAsJoinable
|
|
172
159
|
options.delete(:uniq) if association_type == :has_one
|
173
160
|
|
174
161
|
method_scope = association_type == :has_one ? :protected : :public
|
175
|
-
send(method_scope)
|
162
|
+
#send(method_scope)
|
176
163
|
# has_many :child_users, :through => :child_relationships
|
177
|
-
add_association(relationship.to_s, plural_type, options, join_context, join_value, &block)
|
178
|
-
|
179
|
-
accepts_nested_attributes_for plural_type if nestable
|
164
|
+
add_association(relationship.to_s, plural_type, options, join_context, join_value, nestable, &block)
|
180
165
|
|
181
166
|
if association_type == :has_one
|
182
167
|
define_method singular_type do
|
@@ -207,7 +192,7 @@ module ActsAsJoinable
|
|
207
192
|
role.to_s == "parent" ? "child" : "parent"
|
208
193
|
end
|
209
194
|
|
210
|
-
def add_association(relationship, plural_type, options, join_context, join_value, &block)
|
195
|
+
def add_association(relationship, plural_type, options, join_context, join_value, nestable, &block)
|
211
196
|
eval_options = {:context => join_context}
|
212
197
|
eval_options[:value] = join_value unless join_value.blank?
|
213
198
|
send(:has_many, "#{relationship}_#{plural_type}".to_sym, options) do
|
@@ -218,20 +203,27 @@ module ActsAsJoinable
|
|
218
203
|
EOF
|
219
204
|
end
|
220
205
|
# has_many :users, :through => :child_relationships
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
206
|
+
|
207
|
+
unless self.reflect_on_all_associations.map(&:name).include?(plural_type.to_sym)
|
208
|
+
send(:has_many, plural_type.to_sym, options) do
|
209
|
+
class_eval <<-EOF
|
210
|
+
def construct_join_attributes(associate)
|
211
|
+
super.merge(#{eval_options.inspect})
|
212
|
+
end
|
213
|
+
EOF
|
214
|
+
end
|
215
|
+
|
216
|
+
accepts_nested_attributes_for plural_type.to_sym if nestable
|
227
217
|
end
|
228
218
|
end
|
229
219
|
|
230
220
|
end
|
231
221
|
|
232
222
|
module InstanceMethods
|
233
|
-
|
234
|
-
|
223
|
+
def destroy_relationships
|
224
|
+
conditions = %Q|(`relationships`.parent_type IN ("#{self.class.name}","#{self.class.base_class.name}") AND `relationships`.parent_id = #{self.id}) OR (`relationships`.child_type IN ("#{self.class.name}","#{self.class.base_class.name}") AND `relationships`.child_id = #{self.id})|
|
225
|
+
ActsAsJoinable::Relationship.delete_all(conditions)
|
226
|
+
end
|
235
227
|
end
|
236
228
|
end
|
237
229
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
2
|
|
3
|
-
class ActsAsJoinableTest <
|
3
|
+
class ActsAsJoinableTest < ActiveRecord::TestCase
|
4
4
|
|
5
5
|
context "ActsAsJoinable" do
|
6
6
|
|
@@ -67,6 +67,25 @@ class ActsAsJoinableTest < ActiveSupport::TestCase
|
|
67
67
|
assert_equal 1, Asset.count
|
68
68
|
end
|
69
69
|
|
70
|
+
should "have optimized sql calls" do
|
71
|
+
ActsAsJoinable::Relationship.delete_all
|
72
|
+
group = Group.create!
|
73
|
+
group.posts << Post.create!
|
74
|
+
group.posts << Post.create!
|
75
|
+
assert_queries(1) { Group.all }
|
76
|
+
assert_queries(1) { Group.first }
|
77
|
+
assert_queries(2) do
|
78
|
+
Group.first(:include => [:parent_relationships])
|
79
|
+
end
|
80
|
+
|
81
|
+
# parent, child, and self
|
82
|
+
assert_queries(2) do
|
83
|
+
group.destroy
|
84
|
+
end
|
85
|
+
|
86
|
+
assert_equal 0, ActsAsJoinable::Relationship.count
|
87
|
+
end
|
88
|
+
|
70
89
|
teardown do
|
71
90
|
destroy_models
|
72
91
|
end
|
data/test/test_helper.rb
CHANGED
@@ -26,7 +26,7 @@ require "#{this}/lib/group"
|
|
26
26
|
|
27
27
|
ActiveRecord::Base.class_eval do
|
28
28
|
def self.detonate
|
29
|
-
|
29
|
+
delete_all
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -55,4 +55,16 @@ ActiveSupport::TestCase.class_eval do
|
|
55
55
|
Group.detonate
|
56
56
|
Asset.detonate
|
57
57
|
end
|
58
|
+
end
|
59
|
+
|
60
|
+
ActiveRecord::Base.connection.class.class_eval do
|
61
|
+
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]
|
62
|
+
|
63
|
+
def execute_with_query_record(sql, name = nil, &block)
|
64
|
+
$queries_executed ||= []
|
65
|
+
$queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
|
66
|
+
execute_without_query_record(sql, name, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method_chain :execute, :query_record
|
58
70
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts-as-joinable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.6
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Lance Pollard
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-09-
|
18
|
+
date: 2010-09-20 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|