bigrecord 0.0.5
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/MIT-LICENSE +20 -0
- data/README.rdoc +44 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/doc/bigrecord_specs.rdoc +36 -0
- data/doc/getting_started.rdoc +157 -0
- data/examples/bigrecord.yml +25 -0
- data/generators/bigrecord/bigrecord_generator.rb +17 -0
- data/generators/bigrecord/templates/bigrecord.rake +47 -0
- data/generators/bigrecord_migration/bigrecord_migration_generator.rb +13 -0
- data/generators/bigrecord_migration/templates/migration.rb +9 -0
- data/generators/bigrecord_model/bigrecord_model_generator.rb +28 -0
- data/generators/bigrecord_model/templates/migration.rb +13 -0
- data/generators/bigrecord_model/templates/model.rb +7 -0
- data/generators/bigrecord_model/templates/model_spec.rb +12 -0
- data/init.rb +9 -0
- data/install.rb +22 -0
- data/lib/big_record/abstract_base.rb +1088 -0
- data/lib/big_record/action_view_extensions.rb +266 -0
- data/lib/big_record/ar_associations/association_collection.rb +194 -0
- data/lib/big_record/ar_associations/association_proxy.rb +158 -0
- data/lib/big_record/ar_associations/belongs_to_association.rb +57 -0
- data/lib/big_record/ar_associations/belongs_to_many_association.rb +57 -0
- data/lib/big_record/ar_associations/has_and_belongs_to_many_association.rb +164 -0
- data/lib/big_record/ar_associations/has_many_association.rb +191 -0
- data/lib/big_record/ar_associations/has_one_association.rb +80 -0
- data/lib/big_record/ar_associations.rb +1608 -0
- data/lib/big_record/ar_reflection.rb +223 -0
- data/lib/big_record/attribute_methods.rb +75 -0
- data/lib/big_record/base.rb +618 -0
- data/lib/big_record/br_associations/association_collection.rb +194 -0
- data/lib/big_record/br_associations/association_proxy.rb +153 -0
- data/lib/big_record/br_associations/belongs_to_association.rb +52 -0
- data/lib/big_record/br_associations/belongs_to_many_association.rb +293 -0
- data/lib/big_record/br_associations/cached_item_proxy.rb +194 -0
- data/lib/big_record/br_associations/cached_item_proxy_factory.rb +62 -0
- data/lib/big_record/br_associations/has_and_belongs_to_many_association.rb +168 -0
- data/lib/big_record/br_associations/has_one_association.rb +80 -0
- data/lib/big_record/br_associations.rb +978 -0
- data/lib/big_record/br_reflection.rb +151 -0
- data/lib/big_record/callbacks.rb +367 -0
- data/lib/big_record/connection_adapters/abstract/connection_specification.rb +279 -0
- data/lib/big_record/connection_adapters/abstract/database_statements.rb +175 -0
- data/lib/big_record/connection_adapters/abstract/quoting.rb +58 -0
- data/lib/big_record/connection_adapters/abstract_adapter.rb +190 -0
- data/lib/big_record/connection_adapters/column.rb +491 -0
- data/lib/big_record/connection_adapters/hbase_adapter.rb +432 -0
- data/lib/big_record/connection_adapters/view.rb +27 -0
- data/lib/big_record/connection_adapters.rb +10 -0
- data/lib/big_record/deletion.rb +73 -0
- data/lib/big_record/dynamic_schema.rb +92 -0
- data/lib/big_record/embedded.rb +71 -0
- data/lib/big_record/embedded_associations/association_proxy.rb +148 -0
- data/lib/big_record/family_span_columns.rb +89 -0
- data/lib/big_record/fixtures.rb +1025 -0
- data/lib/big_record/migration.rb +380 -0
- data/lib/big_record/routing_ext.rb +65 -0
- data/lib/big_record/timestamp.rb +51 -0
- data/lib/big_record/validations.rb +830 -0
- data/lib/big_record.rb +125 -0
- data/lib/bigrecord.rb +1 -0
- data/rails/init.rb +9 -0
- data/spec/connections/bigrecord.yml +13 -0
- data/spec/connections/cassandra/connection.rb +2 -0
- data/spec/connections/hbase/connection.rb +2 -0
- data/spec/debug.log +281 -0
- data/spec/integration/br_associations_spec.rb +80 -0
- data/spec/lib/animal.rb +12 -0
- data/spec/lib/book.rb +10 -0
- data/spec/lib/broken_migrations/duplicate_name/20090706182535_add_animals_table.rb +14 -0
- data/spec/lib/broken_migrations/duplicate_name/20090706193019_add_animals_table.rb +9 -0
- data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_books_table.rb +9 -0
- data/spec/lib/broken_migrations/duplicate_version/20090706190623_add_companies_table.rb +9 -0
- data/spec/lib/company.rb +14 -0
- data/spec/lib/embedded/web_link.rb +12 -0
- data/spec/lib/employee.rb +33 -0
- data/spec/lib/migrations/20090706182535_add_animals_table.rb +13 -0
- data/spec/lib/migrations/20090706190623_add_books_table.rb +15 -0
- data/spec/lib/migrations/20090706193019_add_companies_table.rb +14 -0
- data/spec/lib/migrations/20090706194512_add_employees_table.rb +13 -0
- data/spec/lib/migrations/20090706195741_add_zoos_table.rb +13 -0
- data/spec/lib/novel.rb +5 -0
- data/spec/lib/zoo.rb +17 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/unit/abstract_base_spec.rb +287 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +56 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +51 -0
- data/spec/unit/adapters/hbase_adapter_spec.rb +15 -0
- data/spec/unit/ar_associations_spec.rb +8 -0
- data/spec/unit/base_spec.rb +6 -0
- data/spec/unit/br_associations_spec.rb +58 -0
- data/spec/unit/embedded_spec.rb +43 -0
- data/spec/unit/find_spec.rb +34 -0
- data/spec/unit/hash_helper_spec.rb +44 -0
- data/spec/unit/migration_spec.rb +144 -0
- data/spec/unit/model_spec.rb +315 -0
- data/spec/unit/validations_spec.rb +182 -0
- data/tasks/bigrecord_tasks.rake +47 -0
- data/tasks/data_store.rb +46 -0
- data/tasks/gem.rb +22 -0
- data/tasks/rdoc.rb +8 -0
- data/tasks/spec.rb +34 -0
- metadata +189 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module BigRecord
|
|
4
|
+
module BrAssociations
|
|
5
|
+
class AssociationCollection < AssociationProxy #:nodoc:
|
|
6
|
+
def to_ary
|
|
7
|
+
load_target
|
|
8
|
+
@target.to_ary
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def reset
|
|
12
|
+
@loaded = false
|
|
13
|
+
reset_target!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
|
17
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
|
18
|
+
def <<(*records)
|
|
19
|
+
result = true
|
|
20
|
+
load_target
|
|
21
|
+
|
|
22
|
+
# HbaseAdapter doesn't support transactions
|
|
23
|
+
@owner.transaction do
|
|
24
|
+
flatten_deeper(records).each do |record|
|
|
25
|
+
raise_on_type_mismatch(record)
|
|
26
|
+
callback(:before_add, record)
|
|
27
|
+
result &&= insert_record(record) unless @owner.new_record?
|
|
28
|
+
@target << record
|
|
29
|
+
callback(:after_add, record)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result && self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias_method :push, :<<
|
|
37
|
+
alias_method :concat, :<<
|
|
38
|
+
|
|
39
|
+
# Remove all records from this association
|
|
40
|
+
def delete_all
|
|
41
|
+
load_target
|
|
42
|
+
delete(@target)
|
|
43
|
+
reset_target!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Calculate sum using SQL, not Enumerable
|
|
47
|
+
def sum(*args, &block)
|
|
48
|
+
calculate(:sum, *args, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Remove +records+ from this association. Does not destroy +records+.
|
|
52
|
+
def delete(*records)
|
|
53
|
+
records = flatten_deeper(records)
|
|
54
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
|
55
|
+
records.reject! { |record| @target.delete(record) if record.new_record? }
|
|
56
|
+
return if records.empty?
|
|
57
|
+
|
|
58
|
+
# HbaseAdapter doesn't support transactions
|
|
59
|
+
@owner.transaction do
|
|
60
|
+
records.each { |record| callback(:before_remove, record) }
|
|
61
|
+
delete_records(records)
|
|
62
|
+
records.each do |record|
|
|
63
|
+
@target.delete(record)
|
|
64
|
+
callback(:after_remove, record)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
|
70
|
+
def clear
|
|
71
|
+
return self if length.zero? # forces load_target if hasn't happened already
|
|
72
|
+
|
|
73
|
+
if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
|
|
74
|
+
destroy_all
|
|
75
|
+
else
|
|
76
|
+
delete_all
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def destroy_all
|
|
83
|
+
# HbaseAdapter doesn't support transactions
|
|
84
|
+
@owner.transaction do
|
|
85
|
+
each { |record| record.destroy }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
reset_target!
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create(attributes = {})
|
|
92
|
+
# Can't use Base.create since the foreign key may be a protected attribute.
|
|
93
|
+
if attributes.is_a?(Array)
|
|
94
|
+
attributes.collect { |attr| create(attr) }
|
|
95
|
+
else
|
|
96
|
+
record = build(attributes)
|
|
97
|
+
record.save unless @owner.new_record?
|
|
98
|
+
record
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
|
103
|
+
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
|
104
|
+
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
|
105
|
+
def size
|
|
106
|
+
if loaded? && !@reflection.options[:uniq]
|
|
107
|
+
@target.size
|
|
108
|
+
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
|
109
|
+
unsaved_records = Array(@target.detect { |r| r.new_record? })
|
|
110
|
+
unsaved_records.size + count_records
|
|
111
|
+
else
|
|
112
|
+
count_records
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
|
|
117
|
+
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
|
|
118
|
+
def length
|
|
119
|
+
load_target.size
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def empty?
|
|
123
|
+
size.zero?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def uniq(collection = self)
|
|
127
|
+
seen = Set.new
|
|
128
|
+
collection.inject([]) do |kept, record|
|
|
129
|
+
unless seen.include?(record.id)
|
|
130
|
+
kept << record
|
|
131
|
+
seen << record.id
|
|
132
|
+
end
|
|
133
|
+
kept
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Replace this collection with +other_array+
|
|
138
|
+
# This will perform a diff and delete/add only records that have changed.
|
|
139
|
+
def replace(other_array)
|
|
140
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
|
141
|
+
|
|
142
|
+
load_target
|
|
143
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
|
144
|
+
current = @target.size < 100 ? @target : @target.to_set
|
|
145
|
+
|
|
146
|
+
# HbaseAdapter doesn't support transactions
|
|
147
|
+
@owner.transaction do
|
|
148
|
+
delete(@target.select { |v| !other.include?(v) })
|
|
149
|
+
concat(other_array.select { |v| !current.include?(v) })
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
protected
|
|
154
|
+
def reset_target!
|
|
155
|
+
@target = Array.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def find_target
|
|
159
|
+
records =
|
|
160
|
+
if @reflection.options[:finder_sql]
|
|
161
|
+
@reflection.klass.find_by_sql(@finder_sql)
|
|
162
|
+
else
|
|
163
|
+
find(:all)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@reflection.options[:uniq] ? uniq(records) : records
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
def callback(method, record)
|
|
171
|
+
callbacks_for(method).each do |callback|
|
|
172
|
+
case callback
|
|
173
|
+
when Symbol
|
|
174
|
+
@owner.send(callback, record)
|
|
175
|
+
when Proc, Method
|
|
176
|
+
callback.call(@owner, record)
|
|
177
|
+
else
|
|
178
|
+
if callback.respond_to?(method)
|
|
179
|
+
callback.send(method, @owner, record)
|
|
180
|
+
else
|
|
181
|
+
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def callbacks_for(callback_name)
|
|
188
|
+
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
|
189
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module BigRecord
|
|
2
|
+
module BrAssociations
|
|
3
|
+
class AssociationProxy #:nodoc:
|
|
4
|
+
attr_reader :reflection
|
|
5
|
+
alias_method :proxy_respond_to?, :respond_to?
|
|
6
|
+
alias_method :proxy_extend, :extend
|
|
7
|
+
delegate :to_param, :to => :proxy_target
|
|
8
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
|
|
9
|
+
|
|
10
|
+
def initialize(owner, reflection)
|
|
11
|
+
@owner, @reflection = owner, reflection
|
|
12
|
+
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
|
13
|
+
reset
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def proxy_owner
|
|
17
|
+
@owner
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def proxy_reflection
|
|
21
|
+
@reflection
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def proxy_target
|
|
25
|
+
@target
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def respond_to?(symbol, include_priv = false)
|
|
29
|
+
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Explicitly proxy === because the instance method removal above
|
|
33
|
+
# doesn't catch it.
|
|
34
|
+
def ===(other)
|
|
35
|
+
load_target
|
|
36
|
+
other === @target
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def aliased_table_name
|
|
40
|
+
@reflection.klass.table_name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset
|
|
44
|
+
@loaded = false
|
|
45
|
+
@target = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reload
|
|
49
|
+
reset
|
|
50
|
+
load_target
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def loaded?
|
|
54
|
+
@loaded
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def loaded
|
|
58
|
+
@loaded = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def target
|
|
62
|
+
@target
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def target=(target)
|
|
66
|
+
@target = target
|
|
67
|
+
loaded
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
def dependent?
|
|
72
|
+
@reflection.options[:dependent] || false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def quoted_record_ids(records)
|
|
76
|
+
records.map { |record| record.quoted_id }.join(',')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# def interpolate_sql_options!(options, *keys)
|
|
80
|
+
# keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
|
|
81
|
+
# end
|
|
82
|
+
#
|
|
83
|
+
# def interpolate_sql(sql, record = nil)
|
|
84
|
+
# @owner.send(:interpolate_sql, sql, record)
|
|
85
|
+
# end
|
|
86
|
+
#
|
|
87
|
+
# def sanitize_sql(sql)
|
|
88
|
+
# @reflection.klass.send(:sanitize_sql, sql)
|
|
89
|
+
# end
|
|
90
|
+
|
|
91
|
+
def extract_options_from_args!(args)
|
|
92
|
+
@owner.send(:extract_options_from_args!, args)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_belongs_to_association_for(record)
|
|
96
|
+
if @reflection.options[:as]
|
|
97
|
+
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
|
98
|
+
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
|
|
99
|
+
else
|
|
100
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def merge_options_from_reflection!(options)
|
|
105
|
+
options.reverse_merge!(
|
|
106
|
+
:group => @reflection.options[:group],
|
|
107
|
+
:limit => @reflection.options[:limit],
|
|
108
|
+
:offset => @reflection.options[:offset],
|
|
109
|
+
:joins => @reflection.options[:joins],
|
|
110
|
+
:include => @reflection.options[:include],
|
|
111
|
+
:select => @reflection.options[:select]
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
def method_missing(method, *args, &block)
|
|
117
|
+
if load_target
|
|
118
|
+
@target.send(method, *args, &block)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def load_target
|
|
123
|
+
return nil unless defined?(@loaded)
|
|
124
|
+
|
|
125
|
+
if !loaded? and (!@owner.new_record? || foreign_key_present)
|
|
126
|
+
@target = find_target
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@loaded = true
|
|
130
|
+
@target
|
|
131
|
+
rescue BigRecord::RecordNotFound
|
|
132
|
+
reset
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Can be overwritten by associations that might have the foreign key available for an association without
|
|
136
|
+
# having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
|
|
137
|
+
def foreign_key_present
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def raise_on_type_mismatch(record)
|
|
142
|
+
unless record.is_a?(@reflection.klass)
|
|
143
|
+
raise BigRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
|
|
148
|
+
def flatten_deeper(array)
|
|
149
|
+
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module BigRecord
|
|
2
|
+
module BrAssociations
|
|
3
|
+
class BelongsToAssociation < AssociationProxy #:nodoc:
|
|
4
|
+
def create(attributes = {})
|
|
5
|
+
replace(@reflection.klass.create(attributes))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def build(attributes = {})
|
|
9
|
+
replace(@reflection.klass.new(attributes))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def replace(record)
|
|
13
|
+
counter_cache_name = @reflection.counter_cache_column
|
|
14
|
+
|
|
15
|
+
if record.nil?
|
|
16
|
+
if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
|
|
17
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@target = @owner[@reflection.primary_key_name] = nil
|
|
21
|
+
else
|
|
22
|
+
raise_on_type_mismatch(record)
|
|
23
|
+
|
|
24
|
+
if counter_cache_name && !@owner.new_record?
|
|
25
|
+
@reflection.klass.increment_counter(counter_cache_name, record.id)
|
|
26
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@target = (AssociationProxy === record ? record.target : record)
|
|
30
|
+
@owner[@reflection.primary_key_name] = record.id unless record.new_record?
|
|
31
|
+
@updated = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
loaded
|
|
35
|
+
record
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def updated?
|
|
39
|
+
@updated
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
def find_target
|
|
44
|
+
@reflection.klass.find(@owner[@reflection.primary_key_name])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def foreign_key_present
|
|
48
|
+
!@owner[@reflection.primary_key_name].nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
module BigRecord
|
|
2
|
+
module BrAssociations
|
|
3
|
+
class BelongsToManyAssociation < AssociationProxy #:nodoc:
|
|
4
|
+
def to_ary
|
|
5
|
+
load_target
|
|
6
|
+
@target.to_ary
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def reset
|
|
10
|
+
@loaded = false
|
|
11
|
+
reset_target!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
|
15
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
|
16
|
+
def <<(*records)
|
|
17
|
+
result = true
|
|
18
|
+
load_target
|
|
19
|
+
|
|
20
|
+
flatten_deeper(records).each do |record|
|
|
21
|
+
raise_on_type_mismatch(record)
|
|
22
|
+
callback(:before_add, record)
|
|
23
|
+
result &&= insert_record(record)
|
|
24
|
+
@target << record
|
|
25
|
+
callback(:after_add, record)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result && self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias_method :push, :<<
|
|
32
|
+
alias_method :concat, :<<
|
|
33
|
+
|
|
34
|
+
# Remove all records from this association
|
|
35
|
+
def delete_all
|
|
36
|
+
load_target
|
|
37
|
+
delete(@target)
|
|
38
|
+
reset_target!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# # Calculate sum using SQL, not Enumerable
|
|
42
|
+
# def sum(*args, &block)
|
|
43
|
+
# calculate(:sum, *args, &block)
|
|
44
|
+
# end
|
|
45
|
+
|
|
46
|
+
# Remove +records+ from this association. Does not destroy +records+.
|
|
47
|
+
def delete(*records)
|
|
48
|
+
records = flatten_deeper(records)
|
|
49
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
|
50
|
+
records.reject! { |record| @target.delete(record) if record.new_record? }
|
|
51
|
+
return if records.empty?
|
|
52
|
+
|
|
53
|
+
records.each { |record| callback(:before_remove, record) }
|
|
54
|
+
delete_records(records)
|
|
55
|
+
records.each do |record|
|
|
56
|
+
@target.delete(record)
|
|
57
|
+
callback(:after_remove, record)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
|
62
|
+
def clear
|
|
63
|
+
return self if length.zero? # forces load_target if hasn't happened already
|
|
64
|
+
|
|
65
|
+
if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
|
|
66
|
+
destroy_all
|
|
67
|
+
else
|
|
68
|
+
delete_all
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def destroy_all
|
|
75
|
+
# HbaseAdapter doesn't support transactions
|
|
76
|
+
# @owner.transaction do
|
|
77
|
+
each { |record| record.destroy }
|
|
78
|
+
# end
|
|
79
|
+
|
|
80
|
+
reset_target!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create(attributes = {})
|
|
84
|
+
# Can't use Base.create since the foreign key may be a protected attribute.
|
|
85
|
+
if attributes.is_a?(Array)
|
|
86
|
+
attributes.collect { |attr| create(attr) }
|
|
87
|
+
else
|
|
88
|
+
record = build(attributes)
|
|
89
|
+
record.save unless @owner.new_record?
|
|
90
|
+
record
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
|
95
|
+
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
|
|
96
|
+
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
|
|
97
|
+
def size
|
|
98
|
+
@target.size
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
|
|
102
|
+
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
|
|
103
|
+
def length
|
|
104
|
+
load_target.size
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def empty?
|
|
108
|
+
size.zero?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def uniq(collection = self)
|
|
112
|
+
seen = Set.new
|
|
113
|
+
collection.inject([]) do |kept, record|
|
|
114
|
+
unless seen.include?(record.id)
|
|
115
|
+
kept << record
|
|
116
|
+
seen << record.id
|
|
117
|
+
end
|
|
118
|
+
kept
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Replace this collection with +other_array+
|
|
123
|
+
# This will perform a diff and delete/add only records that have changed.
|
|
124
|
+
def replace(other_array)
|
|
125
|
+
if other_array.nil?
|
|
126
|
+
@target = @owner[@reflection.primary_key_name] = nil
|
|
127
|
+
elsif other_array.empty?
|
|
128
|
+
@target = @owner[@reflection.primary_key_name] = []
|
|
129
|
+
else
|
|
130
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
|
131
|
+
|
|
132
|
+
load_target
|
|
133
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
|
134
|
+
current = @target.size < 100 ? @target : @target.to_set
|
|
135
|
+
|
|
136
|
+
delete(@target.select { |v| !other.include?(v) })
|
|
137
|
+
concat(other_array.select { |v| !current.include?(v) })
|
|
138
|
+
@updated = true
|
|
139
|
+
end
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def updated?
|
|
144
|
+
@updated
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
protected
|
|
148
|
+
def reset_target!
|
|
149
|
+
@target = Array.new
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def find_target
|
|
153
|
+
records = find(:all)
|
|
154
|
+
@reflection.options[:uniq] ? uniq(records) : records
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
def callback(method, record)
|
|
159
|
+
callbacks_for(method).each do |callback|
|
|
160
|
+
case callback
|
|
161
|
+
when Symbol
|
|
162
|
+
@owner.send(callback, record)
|
|
163
|
+
when Proc, Method
|
|
164
|
+
callback.call(@owner, record)
|
|
165
|
+
else
|
|
166
|
+
if callback.respond_to?(method)
|
|
167
|
+
callback.send(method, @owner, record)
|
|
168
|
+
else
|
|
169
|
+
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def callbacks_for(callback_name)
|
|
176
|
+
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
|
177
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
public
|
|
187
|
+
def build(attributes = {})
|
|
188
|
+
if attributes.is_a?(Array)
|
|
189
|
+
attributes.collect { |attr| build(attr) }
|
|
190
|
+
else
|
|
191
|
+
record = @reflection.klass.new(attributes)
|
|
192
|
+
set_belongs_to_association_for(record)
|
|
193
|
+
|
|
194
|
+
@target ||= [] unless loaded?
|
|
195
|
+
@target << record
|
|
196
|
+
|
|
197
|
+
record
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Count the number of associated records. All arguments are optional.
|
|
202
|
+
def count(*args)
|
|
203
|
+
load_target
|
|
204
|
+
@target.size
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def find(*args)
|
|
208
|
+
raise ArgumentError, "Only find(:all) is supported" unless args == [:all]
|
|
209
|
+
return [] if @owner[@reflection.primary_key_name].blank?
|
|
210
|
+
|
|
211
|
+
if @reflection.options[:cache]
|
|
212
|
+
# Create the items proxies using the content that is cached
|
|
213
|
+
records = []
|
|
214
|
+
@owner[@reflection.primary_key_name].each do |id|
|
|
215
|
+
records << CachedItemProxyFactory.instance.create(id, @owner, @reflection)
|
|
216
|
+
end
|
|
217
|
+
records
|
|
218
|
+
else
|
|
219
|
+
# Don't throw an exception when the records are not found
|
|
220
|
+
records = []
|
|
221
|
+
@owner[@reflection.primary_key_name].each do |id|
|
|
222
|
+
begin
|
|
223
|
+
records << @reflection.klass.find(id)
|
|
224
|
+
rescue BigRecord::RecordNotFound
|
|
225
|
+
# do nothing
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
records
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
protected
|
|
233
|
+
def load_target
|
|
234
|
+
begin
|
|
235
|
+
if !loaded?
|
|
236
|
+
if @target.is_a?(Array) && @target.any?
|
|
237
|
+
@target = (find_target + @target).uniq
|
|
238
|
+
else
|
|
239
|
+
@target = find_target
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
rescue ActiveRecord::RecordNotFound, BigRecord::RecordNotFound
|
|
243
|
+
reset
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
loaded if target
|
|
247
|
+
target
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def count_records
|
|
251
|
+
count = if has_cached_counter?
|
|
252
|
+
@owner.send(:read_attribute, cached_counter_attribute_name)
|
|
253
|
+
elsif @reflection.options[:counter_sql]
|
|
254
|
+
@reflection.klass.count_by_sql(@counter_sql)
|
|
255
|
+
else
|
|
256
|
+
@reflection.klass.count(:conditions => @counter_sql)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
@target = [] and loaded if count == 0
|
|
260
|
+
|
|
261
|
+
if @reflection.options[:limit]
|
|
262
|
+
count = [ @reflection.options[:limit], count ].min
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
return count
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def has_cached_counter?
|
|
269
|
+
@owner.attribute_present?(cached_counter_attribute_name)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def cached_counter_attribute_name
|
|
273
|
+
"#{@reflection.name}_count"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def insert_record(record)
|
|
277
|
+
@owner[@reflection.primary_key_name] ||= []
|
|
278
|
+
@owner[@reflection.primary_key_name] << record.id unless record.new_record?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def delete_records(records)
|
|
282
|
+
if @owner[@reflection.primary_key_name]
|
|
283
|
+
records.each{|r| @owner[@reflection.primary_key_name].delete(r.id)}
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def target_obsolete?
|
|
288
|
+
false
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|