acts-as-joinable 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|