mongomodel 0.3.3 → 0.3.4

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.
Files changed (37) hide show
  1. data/Appraisals +9 -0
  2. data/Gemfile +2 -2
  3. data/Rakefile +23 -13
  4. data/gemfiles/rails-3.0.gemfile +10 -0
  5. data/gemfiles/rails-3.0.gemfile.lock +73 -0
  6. data/gemfiles/rails-3.1.gemfile +10 -0
  7. data/gemfiles/rails-3.1.gemfile.lock +83 -0
  8. data/lib/mongomodel.rb +0 -1
  9. data/lib/mongomodel/concerns/abstract_class.rb +1 -0
  10. data/lib/mongomodel/concerns/associations.rb +12 -3
  11. data/lib/mongomodel/concerns/associations/base/definition.rb +5 -3
  12. data/lib/mongomodel/concerns/attribute_methods.rb +14 -7
  13. data/lib/mongomodel/concerns/properties.rb +15 -4
  14. data/lib/mongomodel/document/indexes.rb +14 -5
  15. data/lib/mongomodel/document/persistence.rb +7 -5
  16. data/lib/mongomodel/document/scopes.rb +22 -8
  17. data/lib/mongomodel/railtie.rb +2 -3
  18. data/lib/mongomodel/support/collection.rb +3 -1
  19. data/lib/mongomodel/support/map.rb +4 -2
  20. data/lib/mongomodel/support/mongo_options.rb +9 -34
  21. data/lib/mongomodel/support/scope.rb +2 -14
  22. data/lib/mongomodel/version.rb +1 -1
  23. data/mongomodel.gemspec +3 -3
  24. data/spec/mongomodel/concerns/associations/belongs_to_spec.rb +4 -6
  25. data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +123 -123
  26. data/spec/mongomodel/concerns/logging_spec.rb +1 -1
  27. data/spec/mongomodel/document/dynamic_finders_spec.rb +32 -32
  28. data/spec/mongomodel/support/scope_spec.rb +0 -18
  29. data/spec/spec.opts +0 -3
  30. data/spec/spec_helper.rb +4 -4
  31. data/spec/support/helpers/document_finder_stubs.rb +2 -2
  32. data/spec/support/matchers/be_a_subclass_of.rb +1 -1
  33. data/spec/support/matchers/be_truthy.rb +1 -1
  34. data/spec/support/matchers/find_with.rb +4 -4
  35. data/spec/support/matchers/respond_to_boolean.rb +1 -1
  36. data/spec/support/matchers/run_callbacks.rb +1 -1
  37. metadata +23 -74
@@ -6,8 +6,6 @@ module MongoModel
6
6
  included do
7
7
  undef_method :id if method_defined?(:id)
8
8
  property :id, MongoModel::Reference, :as => '_id', :default => lambda { |doc| doc.generate_id }
9
-
10
- class_inheritable_writer :collection_name
11
9
  end
12
10
 
13
11
  # Reload the document from the database. If the document
@@ -93,12 +91,16 @@ module MongoModel
93
91
 
94
92
  def collection_name
95
93
  if superclass.abstract_class?
96
- read_inheritable_attribute(:collection_name) || name.tableize.gsub(/\//, '.')
94
+ @_collection_name || name.tableize.gsub(/\//, '.')
97
95
  else
98
96
  superclass.collection_name
99
97
  end
100
98
  end
101
99
 
100
+ def collection_name=(name)
101
+ @_collection_name = name
102
+ end
103
+
102
104
  def use_type_selector?
103
105
  !superclass.abstract_class?
104
106
  end
@@ -116,11 +118,11 @@ module MongoModel
116
118
  end
117
119
 
118
120
  def save_safely?
119
- @save_safely
121
+ @_save_safely
120
122
  end
121
123
 
122
124
  def save_safely=(val)
123
- @save_safely = val
125
+ @_save_safely = val
124
126
  end
125
127
  end
126
128
 
@@ -23,10 +23,6 @@ module MongoModel
23
23
  current_scope.clone
24
24
  end
25
25
 
26
- def scopes
27
- read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
28
- end
29
-
30
26
  def scope(name, scope)
31
27
  name = name.to_sym
32
28
 
@@ -52,6 +48,28 @@ module MongoModel
52
48
  previous_scope = default_scoping.last || unscoped
53
49
  default_scoping << previous_scope.merge(scope)
54
50
  end
51
+
52
+ def scopes
53
+ @_scopes ||= {}
54
+ end
55
+
56
+ def scopes=(scopes)
57
+ @_scopes = scopes
58
+ end
59
+
60
+ def default_scoping
61
+ @_default_scoping ||= []
62
+ end
63
+
64
+ def default_scoping=(scoping)
65
+ @_default_scoping = scoping
66
+ end
67
+
68
+ def inherited(subclass)
69
+ super
70
+ subclass.scopes = scopes.dup
71
+ subclass.default_scoping = default_scoping.dup
72
+ end
55
73
 
56
74
  protected
57
75
  def with_scope(scope, &block)
@@ -76,10 +94,6 @@ module MongoModel
76
94
  def reset_current_scopes
77
95
  Thread.current[:"#{self}_scopes"] = nil
78
96
  end
79
-
80
- def default_scoping
81
- read_inheritable_attribute(:default_scoping) || write_inheritable_attribute(:default_scoping, [])
82
- end
83
97
  end
84
98
  end
85
99
  end
@@ -1,7 +1,6 @@
1
1
  module MongoModel
2
2
  class Railtie < Rails::Railtie
3
-
4
- config.generators.orm :mongo_model, :migration => false
3
+ config.app_generators.orm :mongo_model, :migration => false
5
4
 
6
5
  rake_tasks do
7
6
  load "mongomodel/tasks/database.rake"
@@ -18,7 +17,7 @@ module MongoModel
18
17
  initializer "mongomodel.database_configuration" do |app|
19
18
  require 'erb'
20
19
 
21
- config = Pathname.new(app.paths.config.to_a.first).join("mongomodel.yml")
20
+ config = Rails.root.join("config", "mongomodel.yml")
22
21
 
23
22
  if File.exists?(config)
24
23
  mongomodel_configuration = YAML::load(ERB.new(IO.read(config)).result)
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
1
3
  module MongoModel
2
4
  class Collection < Array
3
5
  module PropertyDefaults
@@ -14,7 +16,7 @@ module MongoModel
14
16
 
15
17
  ARRAY_CONVERTER = Types.converter_for(Array)
16
18
 
17
- class_inheritable_accessor :type
19
+ class_attribute :type
18
20
  self.type = Object
19
21
 
20
22
  include DocumentParent
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
1
3
  module MongoModel
2
4
  class Map < Hash
3
5
  module PropertyDefaults
@@ -12,10 +14,10 @@ module MongoModel
12
14
  end
13
15
  end
14
16
 
15
- class_inheritable_accessor :from
17
+ class_attribute :from
16
18
  self.from = String
17
19
 
18
- class_inheritable_accessor :to
20
+ class_attribute :to
19
21
  self.to = Object
20
22
 
21
23
  HASH_CONVERTER = Types.converter_for(Hash)
@@ -26,31 +26,16 @@ module MongoModel
26
26
  result = {}
27
27
 
28
28
  (options[:conditions] || {}).each do |k, v|
29
- if k.is_a?(MongoOperator)
30
- key = k.field
31
- else
32
- key = k
33
- end
29
+ key = k.is_a?(MongoOperator) ? k.field : k
34
30
 
35
31
  if property = @model.properties[key]
36
32
  key = property.as
37
-
38
- if k.is_a?(MongoOperator)
39
- value = k.to_mongo_selector(v.is_a?(Array) ? v.map { |i| property.to_query(i) } : property.to_query(v))
40
- else
41
- value = property.to_query(v)
42
- end
33
+ value = v.is_a?(Array) ? v.map { |i| property.to_query(i) } : property.to_query(v);
43
34
  else
44
- converter = Types.converter_for(value.class)
45
-
46
- if k.is_a?(MongoOperator)
47
- value = k.to_mongo_selector(converter.to_mongo(v))
48
- else
49
- value = converter.to_mongo(v)
50
- end
35
+ value = Types.converter_for(v.class).to_mongo(v)
51
36
  end
52
-
53
- result[key] = value
37
+
38
+ result[key] = k.is_a?(MongoOperator) ? k.to_mongo_selector(value) : value
54
39
  end
55
40
 
56
41
  result
@@ -59,7 +44,7 @@ module MongoModel
59
44
  def extract_options(options)
60
45
  result = {}
61
46
 
62
- result[:fields] = options[:select] if options[:select]
47
+ result[:fields] = convert_select(options[:select]) if options[:select]
63
48
  result[:skip] = options[:offset] if options[:offset]
64
49
  result[:limit] = options[:limit] if options[:limit]
65
50
  result[:sort] = MongoOrder.parse(options[:order]).to_sort(@model) if options[:order]
@@ -67,19 +52,9 @@ module MongoModel
67
52
  result
68
53
  end
69
54
 
70
- def convert_order(order)
71
- case order
72
- when Array
73
- order.map { |clause|
74
- key, sort = clause.split(/ /)
75
-
76
- property = @model.properties[key.to_sym]
77
- sort = (sort =~ /desc/i) ? :descending : :ascending
78
-
79
- [property ? property.as : key, sort]
80
- } if order.size > 0
81
- when String, Symbol
82
- convert_order(order.to_s.split(/,/).map { |c| c.strip })
55
+ def convert_select(fields)
56
+ fields.map do |key|
57
+ (@model.properties[key.to_sym].try(:as) || key).to_sym
83
58
  end
84
59
  end
85
60
 
@@ -131,29 +131,17 @@ module MongoModel
131
131
  end
132
132
 
133
133
  def finder_options
134
- @finder_options ||= begin
135
- result = {}
136
-
134
+ @finder_options ||= {}.tap do |result|
137
135
  result[:conditions] = finder_conditions if where_values.any?
138
136
  result[:select] = select_values if select_values.any?
139
137
  result[:order] = order_values if order_values.any?
140
138
  result[:limit] = limit_value if limit_value.present?
141
139
  result[:offset] = offset_value if offset_value.present?
142
-
143
- result
144
140
  end
145
141
  end
146
142
 
147
143
  def options_for_create
148
- @options_for_create ||= begin
149
- result = {}
150
-
151
- finder_conditions.each do |k, v|
152
- result[k] = v unless k.is_a?(MongoModel::MongoOperator)
153
- end
154
-
155
- result
156
- end
144
+ @options_for_create ||= finder_conditions.reject { |k, v| k.is_a?(MongoModel::MongoOperator) }
157
145
  end
158
146
 
159
147
  def respond_to?(method, include_private = false)
@@ -1,3 +1,3 @@
1
1
  module MongoModel
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -14,8 +14,8 @@ Gem::Specification.new do |s|
14
14
  s.required_rubygems_version = ">= 1.3.6"
15
15
  s.rubyforge_project = "mongomodel"
16
16
 
17
- s.add_dependency "activesupport", "~> 3.0.3"
18
- s.add_dependency "activemodel", "~> 3.0.3"
17
+ s.add_dependency "activesupport", "~> 3.0"
18
+ s.add_dependency "activemodel", "~> 3.0"
19
19
  s.add_dependency "mongo", "~> 1.3.0"
20
20
  s.add_dependency "will_paginate", "~> 2.3.15"
21
21
 
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  end
25
25
 
26
26
  s.add_development_dependency "bundler", ">= 1.0.0"
27
- s.add_development_dependency "rspec", "= 1.3.0"
27
+ s.add_development_dependency "rspec", "~> 2.6.0"
28
28
 
29
29
  s.files = `git ls-files`.split("\n")
30
30
  s.require_path = 'lib'
@@ -8,8 +8,6 @@ module MongoModel
8
8
  let(:user) { User.create! }
9
9
  let(:special_user) { SpecialUser.create! }
10
10
 
11
- subject { Article.new }
12
-
13
11
  context "when uninitialized" do
14
12
  it "should be nil" do
15
13
  subject.user.should be_nil
@@ -79,6 +77,8 @@ module MongoModel
79
77
  belongs_to :user
80
78
  end
81
79
 
80
+ subject { Article.new }
81
+
82
82
  it_should_behave_like "assigning correct class to belongs_to association"
83
83
 
84
84
  describe "setting a different class type" do
@@ -92,8 +92,6 @@ module MongoModel
92
92
  end
93
93
 
94
94
  describe "#build_user" do
95
- subject { Article.new }
96
-
97
95
  let(:user) { subject.build_user(:id => '123') }
98
96
 
99
97
  it "should return a new unsaved user with the given attributes" do
@@ -104,8 +102,6 @@ module MongoModel
104
102
  end
105
103
 
106
104
  describe "#create_user" do
107
- subject { Article.new }
108
-
109
105
  it "should return a new saved user with the given attributes" do
110
106
  user = subject.create_user(:id => '123')
111
107
  user.should be_an_instance_of(User)
@@ -120,6 +116,8 @@ module MongoModel
120
116
  belongs_to :user, :polymorphic => true
121
117
  end
122
118
 
119
+ subject { Article.new }
120
+
123
121
  define_class(:NonUser, Document)
124
122
 
125
123
  let(:non_user) { NonUser.create! }
@@ -13,6 +13,129 @@ module MongoModel
13
13
  end
14
14
  end
15
15
 
16
+ shared_examples_for "accessing and manipulating a has_many :by => :ids association" do
17
+ it "should access chapters" do
18
+ subject.chapters.should == [chapter1, chapter2]
19
+ end
20
+
21
+ it "should access chapter ids through association" do
22
+ subject.chapters.ids.should == [chapter1.id, chapter2.id]
23
+ end
24
+
25
+ it "should have chapter ids" do
26
+ subject.chapter_ids.should == [chapter1.id, chapter2.id]
27
+ end
28
+
29
+ it "should add chapters with <<" do
30
+ subject.chapters << chapter3
31
+ subject.chapters.should == [chapter1, chapter2, chapter3]
32
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
33
+ end
34
+
35
+ it "should add/change chapters with []=" do
36
+ subject.chapters[2] = chapter3
37
+ subject.chapters.should == [chapter1, chapter2, chapter3]
38
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
39
+ end
40
+
41
+ it "should add chapters with concat" do
42
+ subject.chapters.concat([chapter3])
43
+ subject.chapters.should == [chapter1, chapter2, chapter3]
44
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
45
+ end
46
+
47
+ it "should insert chapters" do
48
+ subject.chapters.insert(1, chapter3)
49
+ subject.chapters.should == [chapter1, chapter3, chapter2]
50
+ subject.chapter_ids.should == [chapter1.id, chapter3.id, chapter2.id]
51
+ end
52
+
53
+ it "should replace chapters" do
54
+ subject.chapters.replace([chapter2, chapter3])
55
+ subject.chapters.should == [chapter2, chapter3]
56
+ subject.chapter_ids.should == [chapter2.id, chapter3.id]
57
+ end
58
+
59
+ it "should add chapters with push" do
60
+ subject.chapters.push(chapter3)
61
+ subject.chapters.should == [chapter1, chapter2, chapter3]
62
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
63
+ end
64
+
65
+ it "should add chapters with unshift" do
66
+ subject.chapters.unshift(chapter3)
67
+ subject.chapters.should == [chapter3, chapter1, chapter2]
68
+ subject.chapter_ids.should == [chapter3.id, chapter1.id, chapter2.id]
69
+ end
70
+
71
+ it "should clear chapters" do
72
+ subject.chapters.clear
73
+ subject.chapters.should be_empty
74
+ subject.chapter_ids.should be_empty
75
+ end
76
+
77
+ it "should remove chapters with delete" do
78
+ subject.chapters.delete(chapter1)
79
+ subject.chapters.should == [chapter2]
80
+ subject.chapter_ids.should == [chapter2.id]
81
+ end
82
+
83
+ it "should remove chapters with delete_at" do
84
+ subject.chapters.delete_at(0)
85
+ subject.chapters.should == [chapter2]
86
+ subject.chapter_ids.should == [chapter2.id]
87
+ end
88
+
89
+ it "should remove chapters with delete_if" do
90
+ subject.chapters.delete_if { |c| c.id == chapter1.id }
91
+ subject.chapters.should == [chapter2]
92
+ subject.chapter_ids.should == [chapter2.id]
93
+ end
94
+
95
+ it "should build a chapter" do
96
+ chapter4 = subject.chapters.build(:id => '4')
97
+ subject.chapters.should == [chapter1, chapter2, chapter4]
98
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter4.id]
99
+
100
+ chapter4.should be_a_new_record
101
+ chapter4.id.should == '4'
102
+ end
103
+
104
+ it "should create a chapter" do
105
+ chapter4 = subject.chapters.create(:id => '4')
106
+ subject.chapters.should == [chapter1, chapter2, chapter4]
107
+ subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter4.id]
108
+
109
+ chapter4.should_not be_a_new_record
110
+ chapter4.id.should == '4'
111
+ end
112
+
113
+ it "should find chapters" do
114
+ # Create bogus chapters
115
+ Chapter.create!(:id => '999')
116
+ Chapter.create!(:id => '998')
117
+
118
+ result = subject.chapters.order(:id.desc)
119
+ result.should == [chapter2, chapter1]
120
+ end
121
+
122
+ describe "adding a non-chapter" do
123
+ def self.should_raise(message, &block)
124
+ it "should raise an AsssociationTypeMismatch error when #{message}" do
125
+ lambda { instance_eval(&block) }.should raise_error(AssociationTypeMismatch, "expected instance of Chapter but got NonChapter")
126
+ end
127
+ end
128
+
129
+ should_raise("assigning an array containing non-chapters") { subject.chapters = [nonchapter] }
130
+ should_raise("adding a non-chapter using <<") { subject.chapters << nonchapter }
131
+ should_raise("adding non-chapters with concat") { subject.chapters.concat([nonchapter]) }
132
+ should_raise("inserting chapters") { subject.chapters.insert(1, nonchapter) }
133
+ should_raise("replacing chapters") { subject.chapters.replace([nonchapter]) }
134
+ should_raise("addding chapters with push") { subject.chapters.push(nonchapter) }
135
+ should_raise("addding chapters with unshift") { subject.chapters.unshift(nonchapter) }
136
+ end
137
+ end
138
+
16
139
  specs_for(Document, EmbeddedDocument) do
17
140
  describe "has_many :by => :ids association" do
18
141
  define_class(:Chapter, Document)
@@ -39,129 +162,6 @@ module MongoModel
39
162
  end
40
163
  end
41
164
 
42
- shared_examples_for "accessing and manipulating a has_many :by => :ids association" do
43
- it "should access chapters" do
44
- subject.chapters.should == [chapter1, chapter2]
45
- end
46
-
47
- it "should access chapter ids through association" do
48
- subject.chapters.ids.should == [chapter1.id, chapter2.id]
49
- end
50
-
51
- it "should have chapter ids" do
52
- subject.chapter_ids.should == [chapter1.id, chapter2.id]
53
- end
54
-
55
- it "should add chapters with <<" do
56
- subject.chapters << chapter3
57
- subject.chapters.should == [chapter1, chapter2, chapter3]
58
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
59
- end
60
-
61
- it "should add/change chapters with []=" do
62
- subject.chapters[2] = chapter3
63
- subject.chapters.should == [chapter1, chapter2, chapter3]
64
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
65
- end
66
-
67
- it "should add chapters with concat" do
68
- subject.chapters.concat([chapter3])
69
- subject.chapters.should == [chapter1, chapter2, chapter3]
70
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
71
- end
72
-
73
- it "should insert chapters" do
74
- subject.chapters.insert(1, chapter3)
75
- subject.chapters.should == [chapter1, chapter3, chapter2]
76
- subject.chapter_ids.should == [chapter1.id, chapter3.id, chapter2.id]
77
- end
78
-
79
- it "should replace chapters" do
80
- subject.chapters.replace([chapter2, chapter3])
81
- subject.chapters.should == [chapter2, chapter3]
82
- subject.chapter_ids.should == [chapter2.id, chapter3.id]
83
- end
84
-
85
- it "should add chapters with push" do
86
- subject.chapters.push(chapter3)
87
- subject.chapters.should == [chapter1, chapter2, chapter3]
88
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter3.id]
89
- end
90
-
91
- it "should add chapters with unshift" do
92
- subject.chapters.unshift(chapter3)
93
- subject.chapters.should == [chapter3, chapter1, chapter2]
94
- subject.chapter_ids.should == [chapter3.id, chapter1.id, chapter2.id]
95
- end
96
-
97
- it "should clear chapters" do
98
- subject.chapters.clear
99
- subject.chapters.should be_empty
100
- subject.chapter_ids.should be_empty
101
- end
102
-
103
- it "should remove chapters with delete" do
104
- subject.chapters.delete(chapter1)
105
- subject.chapters.should == [chapter2]
106
- subject.chapter_ids.should == [chapter2.id]
107
- end
108
-
109
- it "should remove chapters with delete_at" do
110
- subject.chapters.delete_at(0)
111
- subject.chapters.should == [chapter2]
112
- subject.chapter_ids.should == [chapter2.id]
113
- end
114
-
115
- it "should remove chapters with delete_if" do
116
- subject.chapters.delete_if { |c| c.id == chapter1.id }
117
- subject.chapters.should == [chapter2]
118
- subject.chapter_ids.should == [chapter2.id]
119
- end
120
-
121
- it "should build a chapter" do
122
- chapter4 = subject.chapters.build(:id => '4')
123
- subject.chapters.should == [chapter1, chapter2, chapter4]
124
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter4.id]
125
-
126
- chapter4.should be_a_new_record
127
- chapter4.id.should == '4'
128
- end
129
-
130
- it "should create a chapter" do
131
- chapter4 = subject.chapters.create(:id => '4')
132
- subject.chapters.should == [chapter1, chapter2, chapter4]
133
- subject.chapter_ids.should == [chapter1.id, chapter2.id, chapter4.id]
134
-
135
- chapter4.should_not be_a_new_record
136
- chapter4.id.should == '4'
137
- end
138
-
139
- it "should find chapters" do
140
- # Create bogus chapters
141
- Chapter.create!(:id => '999')
142
- Chapter.create!(:id => '998')
143
-
144
- result = subject.chapters.order(:id.desc)
145
- result.should == [chapter2, chapter1]
146
- end
147
-
148
- describe "adding a non-chapter" do
149
- def self.should_raise(message, &block)
150
- it "should raise an AsssociationTypeMismatch error when #{message}" do
151
- lambda { instance_eval(&block) }.should raise_error(AssociationTypeMismatch, "expected instance of Chapter but got NonChapter")
152
- end
153
- end
154
-
155
- should_raise("assigning an array containing non-chapters") { subject.chapters = [nonchapter] }
156
- should_raise("adding a non-chapter using <<") { subject.chapters << nonchapter }
157
- should_raise("adding non-chapters with concat") { subject.chapters.concat([nonchapter]) }
158
- should_raise("inserting chapters") { subject.chapters.insert(1, nonchapter) }
159
- should_raise("replacing chapters") { subject.chapters.replace([nonchapter]) }
160
- should_raise("addding chapters with push") { subject.chapters.push(nonchapter) }
161
- should_raise("addding chapters with unshift") { subject.chapters.unshift(nonchapter) }
162
- end
163
- end
164
-
165
165
  context "with chapters set" do
166
166
  subject { Book.new(:chapters => [chapter1, chapter2]) }
167
167
  it_should_behave_like "accessing and manipulating a has_many :by => :ids association"