massive_record 0.1.1 → 0.2.0.beta
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/CHANGELOG.md +28 -5
- data/Gemfile.lock +12 -12
- data/README.md +29 -1
- data/lib/massive_record/adapters/initialize.rb +18 -0
- data/lib/massive_record/adapters/thrift/adapter.rb +25 -0
- data/lib/massive_record/adapters/thrift/column_family.rb +24 -0
- data/lib/massive_record/adapters/thrift/connection.rb +73 -0
- data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase.rb +0 -0
- data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase_constants.rb +0 -0
- data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase_types.rb +0 -0
- data/lib/massive_record/adapters/thrift/row.rb +150 -0
- data/lib/massive_record/adapters/thrift/scanner.rb +59 -0
- data/lib/massive_record/adapters/thrift/table.rb +169 -0
- data/lib/massive_record/orm/attribute_methods/read.rb +2 -1
- data/lib/massive_record/orm/base.rb +61 -3
- data/lib/massive_record/orm/coders/chained.rb +71 -0
- data/lib/massive_record/orm/coders/json.rb +17 -0
- data/lib/massive_record/orm/coders/yaml.rb +15 -0
- data/lib/massive_record/orm/coders.rb +3 -0
- data/lib/massive_record/orm/errors.rb +15 -2
- data/lib/massive_record/orm/finders/scope.rb +166 -0
- data/lib/massive_record/orm/finders.rb +45 -24
- data/lib/massive_record/orm/persistence.rb +4 -4
- data/lib/massive_record/orm/relations/interface.rb +170 -0
- data/lib/massive_record/orm/relations/metadata.rb +150 -0
- data/lib/massive_record/orm/relations/proxy/references_many.rb +229 -0
- data/lib/massive_record/orm/relations/proxy/references_one.rb +40 -0
- data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +49 -0
- data/lib/massive_record/orm/relations/proxy.rb +174 -0
- data/lib/massive_record/orm/relations.rb +6 -0
- data/lib/massive_record/orm/schema/column_interface.rb +1 -1
- data/lib/massive_record/orm/schema/field.rb +62 -27
- data/lib/massive_record/orm/single_table_inheritance.rb +21 -0
- data/lib/massive_record/version.rb +1 -1
- data/lib/massive_record/wrapper/adapter.rb +6 -0
- data/lib/massive_record/wrapper/base.rb +6 -7
- data/lib/massive_record/wrapper/cell.rb +9 -32
- data/lib/massive_record/wrapper/column_families_collection.rb +2 -2
- data/lib/massive_record/wrapper/errors.rb +10 -0
- data/lib/massive_record/wrapper/tables_collection.rb +1 -1
- data/lib/massive_record.rb +5 -12
- data/spec/orm/cases/attribute_methods_spec.rb +5 -1
- data/spec/orm/cases/base_spec.rb +77 -4
- data/spec/orm/cases/column_spec.rb +1 -1
- data/spec/orm/cases/finder_default_scope.rb +53 -0
- data/spec/orm/cases/finder_scope_spec.rb +288 -0
- data/spec/orm/cases/finders_spec.rb +56 -13
- data/spec/orm/cases/persistence_spec.rb +20 -5
- data/spec/orm/cases/single_table_inheritance_spec.rb +26 -0
- data/spec/orm/cases/table_spec.rb +1 -1
- data/spec/orm/cases/timestamps_spec.rb +16 -16
- data/spec/orm/coders/chained_spec.rb +73 -0
- data/spec/orm/coders/json_spec.rb +6 -0
- data/spec/orm/coders/yaml_spec.rb +6 -0
- data/spec/orm/models/best_friend.rb +7 -0
- data/spec/orm/models/friend.rb +4 -0
- data/spec/orm/models/person.rb +20 -6
- data/spec/orm/models/{person_with_timestamps.rb → person_with_timestamp.rb} +1 -1
- data/spec/orm/models/test_class.rb +3 -0
- data/spec/orm/relations/interface_spec.rb +207 -0
- data/spec/orm/relations/metadata_spec.rb +202 -0
- data/spec/orm/relations/proxy/references_many_spec.rb +624 -0
- data/spec/orm/relations/proxy/references_one_polymorphic_spec.rb +106 -0
- data/spec/orm/relations/proxy/references_one_spec.rb +111 -0
- data/spec/orm/relations/proxy_spec.rb +13 -0
- data/spec/orm/schema/field_spec.rb +101 -2
- data/spec/shared/orm/coders/an_orm_coder.rb +14 -0
- data/spec/shared/orm/relations/proxy.rb +154 -0
- data/spec/shared/orm/relations/singular_proxy.rb +68 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/thrift/cases/encoding_spec.rb +28 -7
- data/spec/wrapper/cases/adapter_spec.rb +9 -0
- data/spec/wrapper/cases/connection_spec.rb +13 -10
- data/spec/wrapper/cases/table_spec.rb +85 -85
- metadata +74 -22
- data/TODO.md +0 -8
- data/lib/massive_record/exceptions.rb +0 -11
- data/lib/massive_record/wrapper/column_family.rb +0 -22
- data/lib/massive_record/wrapper/connection.rb +0 -71
- data/lib/massive_record/wrapper/row.rb +0 -173
- data/lib/massive_record/wrapper/scanner.rb +0 -61
- data/lib/massive_record/wrapper/table.rb +0 -149
- data/spec/orm/cases/hbase/connection_spec.rb +0 -13
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support/json'
|
2
|
+
|
3
|
+
module MassiveRecord
|
4
|
+
module ORM
|
5
|
+
module Coders
|
6
|
+
class JSON
|
7
|
+
def dump(object)
|
8
|
+
ActiveSupport::JSON.encode(object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def load(json)
|
12
|
+
ActiveSupport::JSON.decode(json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -33,9 +33,9 @@ module MassiveRecord
|
|
33
33
|
|
34
34
|
class ColumnFamiliesMissingError < MassiveRecordError
|
35
35
|
attr_reader :missing_column_families
|
36
|
-
def initialize(missing_column_families)
|
36
|
+
def initialize(klass, missing_column_families)
|
37
37
|
@missing_column_families = missing_column_families
|
38
|
-
super("hbase are missing some column families: #{@missing_column_families.join(' ')}. Please migrate the database.")
|
38
|
+
super("hbase are missing some column families for class '#{klass.to_s}', table '#{klass.table_name}': #{@missing_column_families.join(' ')}. Please migrate the database.")
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
@@ -52,5 +52,18 @@ module MassiveRecord
|
|
52
52
|
# Raised if you try to save a record which is read only
|
53
53
|
class ReadOnlyRecord < MassiveRecordError
|
54
54
|
end
|
55
|
+
|
56
|
+
|
57
|
+
# Raised when a relation s already defined
|
58
|
+
class RelationAlreadyDefined < MassiveRecordError
|
59
|
+
end
|
60
|
+
|
61
|
+
# Raised if proxy_target in a relation proxy does not match what the proxy expects
|
62
|
+
class RelationTypeMismatch < MassiveRecordError
|
63
|
+
end
|
64
|
+
|
65
|
+
# Raised when an attribute is decoded from the database, but the type returned does not match what is expected
|
66
|
+
class SerializationTypeMismatch < MassiveRecordError
|
67
|
+
end
|
55
68
|
end
|
56
69
|
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module MassiveRecord
|
2
|
+
module ORM
|
3
|
+
module Finders
|
4
|
+
|
5
|
+
#
|
6
|
+
# A finder scope's jobs is to contain and build up
|
7
|
+
# limitations and meta data of a DB query about to being
|
8
|
+
# executed, allowing us to call for instance
|
9
|
+
#
|
10
|
+
# User.select(:info).limit(5) or
|
11
|
+
# User.select(:info).find(a_user_id)
|
12
|
+
#
|
13
|
+
# Each call adds restrictions or info about the query about
|
14
|
+
# to be executed, and the proxy will act as an Enumerable object
|
15
|
+
# when asking for multiple values
|
16
|
+
#
|
17
|
+
class Scope
|
18
|
+
MULTI_VALUE_METHODS = %w(select)
|
19
|
+
SINGLE_VALUE_METHODS = %w(limit)
|
20
|
+
|
21
|
+
attr_accessor *MULTI_VALUE_METHODS.collect { |m| m + "_values" }
|
22
|
+
attr_accessor *SINGLE_VALUE_METHODS.collect { |m| m + "_value" }
|
23
|
+
attr_accessor :loaded, :klass
|
24
|
+
alias :loaded? :loaded
|
25
|
+
|
26
|
+
|
27
|
+
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
|
28
|
+
|
29
|
+
|
30
|
+
def initialize(klass, options = {})
|
31
|
+
@klass = klass
|
32
|
+
@extra_finder_options = {}
|
33
|
+
|
34
|
+
reset
|
35
|
+
reset_single_values_options
|
36
|
+
reset_multi_values_options
|
37
|
+
|
38
|
+
apply_finder_options(options[:find_options]) if options.has_key? :find_options
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def reset
|
43
|
+
@loaded = false
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
#
|
49
|
+
# Multi value options
|
50
|
+
#
|
51
|
+
|
52
|
+
def select(*select)
|
53
|
+
self.select_values |= select.flatten.compact.collect(&:to_s)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
#
|
59
|
+
# Single value options
|
60
|
+
#
|
61
|
+
|
62
|
+
def limit(limit)
|
63
|
+
self.limit_value = limit
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
def ==(other)
|
72
|
+
case other
|
73
|
+
when Scope
|
74
|
+
object_id == other.object_id
|
75
|
+
when Array
|
76
|
+
to_a == other
|
77
|
+
else
|
78
|
+
raise "Don't know how to compare #{self.class} with #{other.class}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
def find(*args)
|
85
|
+
options = args.extract_options!.to_options
|
86
|
+
apply_finder_options(options)
|
87
|
+
args << options.merge(find_options)
|
88
|
+
|
89
|
+
klass.do_find(*args)
|
90
|
+
end
|
91
|
+
|
92
|
+
def all(options = {})
|
93
|
+
apply_finder_options(options)
|
94
|
+
to_a
|
95
|
+
end
|
96
|
+
|
97
|
+
def first(options = {})
|
98
|
+
apply_finder_options(options)
|
99
|
+
limit(1).to_a.first
|
100
|
+
end
|
101
|
+
|
102
|
+
def last(*args)
|
103
|
+
raise "Sorry, not implemented!"
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
def to_a
|
109
|
+
return @records if loaded?
|
110
|
+
@records = load_records
|
111
|
+
@records = [@records] unless @records.is_a? Array
|
112
|
+
@records
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
|
121
|
+
def load_records
|
122
|
+
@records = klass.do_find(:all, find_options)
|
123
|
+
@loaded = true
|
124
|
+
@records
|
125
|
+
end
|
126
|
+
|
127
|
+
def find_options
|
128
|
+
options = {}
|
129
|
+
|
130
|
+
SINGLE_VALUE_METHODS.each do |m|
|
131
|
+
value = send("#{m}_value") and options[m.to_sym] = value
|
132
|
+
end
|
133
|
+
|
134
|
+
MULTI_VALUE_METHODS.each do |m|
|
135
|
+
values = send("#{m}_values") and values.any? and options[m.to_sym] = values
|
136
|
+
end
|
137
|
+
|
138
|
+
options.merge(@extra_finder_options)
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
def apply_finder_options(options)
|
144
|
+
options.each do |scope_method, arguments|
|
145
|
+
if respond_to? scope_method
|
146
|
+
send(scope_method, arguments)
|
147
|
+
else
|
148
|
+
@extra_finder_options[scope_method] = arguments
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
def reset_single_values_options
|
157
|
+
SINGLE_VALUE_METHODS.each { |m| instance_variable_set("@#{m}_value", nil) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def reset_multi_values_options
|
161
|
+
MULTI_VALUE_METHODS.each { |m| instance_variable_set("@#{m}_values", []) }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -3,23 +3,30 @@ module MassiveRecord
|
|
3
3
|
module Finders
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
included do
|
7
|
+
class << self
|
8
|
+
delegate :find, :first, :last, :all, :select, :limit, :to => :finder_scope
|
9
|
+
end
|
10
|
+
|
11
|
+
class_attribute :default_scoping, :instance_writer => false
|
12
|
+
end
|
13
|
+
|
6
14
|
module ClassMethods
|
7
|
-
#
|
8
|
-
# Interface for retrieving objects based on key.
|
9
|
-
# Has some convenience behaviour like find :first, :last, :all.
|
10
|
-
#
|
11
|
-
def find(*args)
|
15
|
+
def do_find(*args) # :nodoc:
|
12
16
|
options = args.extract_options!.to_options
|
13
17
|
raise ArgumentError.new("At least one argument required!") if args.empty?
|
14
18
|
raise RecordNotFound.new("Can't find a #{model_name.human} without an ID.") if args.first.nil?
|
15
19
|
raise ArgumentError.new("Sorry, conditions are not supported!") if options.has_key? :conditions
|
20
|
+
|
21
|
+
skip_expected_result_check = options.delete(:skip_expected_result_check)
|
22
|
+
|
16
23
|
args << options
|
17
24
|
|
18
25
|
type = args.shift if args.first.is_a? Symbol
|
19
26
|
find_many = type == :all
|
20
27
|
expected_result_size = nil
|
21
28
|
|
22
|
-
return (find_many ? [] :
|
29
|
+
return (find_many ? [] : raise(RecordNotFound.new("Could not find #{model_name} with id=#{args.first}"))) unless table.exists?
|
23
30
|
|
24
31
|
result_from_table = if type
|
25
32
|
table.send(type, *args) # first() / all()
|
@@ -43,7 +50,7 @@ module MassiveRecord
|
|
43
50
|
# we have no expectations on the returned rows' ids)
|
44
51
|
unless type || result_from_table.blank?
|
45
52
|
if find_many
|
46
|
-
result_from_table.select! { |result| what_to_find.include? result.id }
|
53
|
+
result_from_table.select! { |result| what_to_find.include? result.try(:id) }
|
47
54
|
else
|
48
55
|
if result_from_table.id != what_to_find
|
49
56
|
result_from_table = nil
|
@@ -53,7 +60,7 @@ module MassiveRecord
|
|
53
60
|
|
54
61
|
raise RecordNotFound.new("Could not find #{model_name} with id=#{what_to_find}") if result_from_table.blank? && type.nil?
|
55
62
|
|
56
|
-
if find_many && expected_result_size && expected_result_size != result_from_table.length
|
63
|
+
if find_many && !skip_expected_result_check && expected_result_size && expected_result_size != result_from_table.length
|
57
64
|
raise RecordNotFound.new("Expected to find #{expected_result_size} records, but found only #{result_from_table.length}")
|
58
65
|
end
|
59
66
|
|
@@ -64,19 +71,9 @@ module MassiveRecord
|
|
64
71
|
find_many ? records : records.first
|
65
72
|
end
|
66
73
|
|
67
|
-
def first(*args)
|
68
|
-
find(:first, *args)
|
69
|
-
end
|
70
|
-
|
71
|
-
def last(*args)
|
72
|
-
raise "Sorry, not implemented!"
|
73
|
-
end
|
74
|
-
|
75
|
-
def all(*args)
|
76
|
-
find(:all, *args)
|
77
|
-
end
|
78
|
-
|
79
74
|
def find_in_batches(*args)
|
75
|
+
return unless table.exists?
|
76
|
+
|
80
77
|
table.find_in_batches(*args) do |rows|
|
81
78
|
records = rows.collect do |row|
|
82
79
|
instantiate(transpose_hbase_columns_to_record_attributes(row))
|
@@ -99,6 +96,26 @@ module MassiveRecord
|
|
99
96
|
end
|
100
97
|
|
101
98
|
|
99
|
+
def finder_scope
|
100
|
+
default_scoping || unscoped
|
101
|
+
end
|
102
|
+
|
103
|
+
def default_scope(scope)
|
104
|
+
self.default_scoping = case scope
|
105
|
+
when Scope, nil
|
106
|
+
scope
|
107
|
+
when Hash
|
108
|
+
Scope.new(self, :find_options => scope)
|
109
|
+
else
|
110
|
+
raise "Don't know how to set scope with #{scope.class}."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def unscoped
|
115
|
+
Scope.new(self)
|
116
|
+
end
|
117
|
+
|
118
|
+
|
102
119
|
private
|
103
120
|
|
104
121
|
def transpose_hbase_columns_to_record_attributes(row)
|
@@ -109,15 +126,19 @@ module MassiveRecord
|
|
109
126
|
# Parse the schema to populate the instance attributes
|
110
127
|
attributes_schema.each do |key, field|
|
111
128
|
cell = row.columns[field.unique_name]
|
112
|
-
attributes[field.name] = cell.nil? ? nil : cell.
|
129
|
+
attributes[field.name] = cell.nil? ? nil : field.decode(cell.value)
|
113
130
|
end
|
114
131
|
attributes
|
115
132
|
end
|
116
133
|
|
117
134
|
def instantiate(record)
|
118
|
-
|
119
|
-
|
120
|
-
|
135
|
+
model = if klass = record[inheritance_attribute] and klass.present?
|
136
|
+
klass.constantize.allocate
|
137
|
+
else
|
138
|
+
allocate
|
139
|
+
end
|
140
|
+
|
141
|
+
model.init_with('attributes' => record)
|
121
142
|
end
|
122
143
|
end
|
123
144
|
end
|
@@ -30,7 +30,7 @@ module MassiveRecord
|
|
30
30
|
|
31
31
|
|
32
32
|
def reload
|
33
|
-
self.attributes_raw = self.class.find(id).attributes
|
33
|
+
self.attributes_raw = self.class.find(id).attributes if persisted?
|
34
34
|
self
|
35
35
|
end
|
36
36
|
|
@@ -64,7 +64,7 @@ module MassiveRecord
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def destroy
|
67
|
-
@destroyed = row_for_record.destroy and freeze
|
67
|
+
@destroyed = (persisted? ? row_for_record.destroy : true) and freeze
|
68
68
|
end
|
69
69
|
alias_method :delete, :destroy
|
70
70
|
|
@@ -155,7 +155,7 @@ module MassiveRecord
|
|
155
155
|
self.class.table.save
|
156
156
|
end
|
157
157
|
|
158
|
-
raise ColumnFamiliesMissingError.new(calculate_missing_family_names) if !calculate_missing_family_names.empty?
|
158
|
+
raise ColumnFamiliesMissingError.new(self.class, calculate_missing_family_names) if !calculate_missing_family_names.empty?
|
159
159
|
end
|
160
160
|
|
161
161
|
#
|
@@ -190,7 +190,7 @@ module MassiveRecord
|
|
190
190
|
|
191
191
|
attributes_schema.each do |attr_name, orm_field|
|
192
192
|
next unless only_attr_names.empty? || only_attr_names.include?(attr_name)
|
193
|
-
values[orm_field.column_family.name][orm_field.column] = send(attr_name)
|
193
|
+
values[orm_field.column_family.name][orm_field.column] = orm_field.encode(send(attr_name))
|
194
194
|
end
|
195
195
|
|
196
196
|
values
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module MassiveRecord
|
2
|
+
module ORM
|
3
|
+
module Relations
|
4
|
+
module Interface
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :relations, :instance_writer => false
|
9
|
+
self.relations = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
#
|
15
|
+
# Used to define a references one relation. Example of usage:
|
16
|
+
#
|
17
|
+
# class Person < MassiveRecord::ORM::Table
|
18
|
+
# column_family :info do
|
19
|
+
# field :name
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# references_one :boss, :class_name => "Person", :store_in => :info
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# First argument is the name of the relation. class_name and foreign key is calculated from it, if none given.
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# Options, all optional:
|
29
|
+
#
|
30
|
+
# <tt>class_name</tt>:: Class name is calculated from name, but can be overridden here.
|
31
|
+
# <tt>polymorphic</tt>:: Set it to true for make the association polymorphic. Will use foreign_key,
|
32
|
+
# remove the "_id" (if it's there) and add _type for it's reading/writing of type.
|
33
|
+
# <tt>foreign_key</tt>:: Foreign key is calculated from name suffixed by _id as default.
|
34
|
+
# <tt>store_in</tt>:: Send in the column family to store foreign key in. If none given,
|
35
|
+
# you should define the foreign key method in class if it can be
|
36
|
+
# calculated from another attributes in your class.
|
37
|
+
# <tt>find_with</tt>:: Assign it to a Proc and we will call it with the proxy_owner if you need complete
|
38
|
+
# control over how you retrieve your record.
|
39
|
+
# As a default TargetClass.find(foreign_key_method) is used.
|
40
|
+
#
|
41
|
+
def references_one(name, *args)
|
42
|
+
metadata = set_up_relation('references_one', name, *args)
|
43
|
+
|
44
|
+
create_references_one_accessors(metadata)
|
45
|
+
create_references_one_polymorphic_accessors(metadata) if metadata.polymorphic?
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
#
|
50
|
+
# Used to define a reference many relation. Example of usage:
|
51
|
+
#
|
52
|
+
# class Person < MassiveRecord::ORM::Table
|
53
|
+
# column_family :info do
|
54
|
+
# field :name
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# references_many :cars, :store_in => :info
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# First argument is the name of the relation. class_name and attribute for foreign keys are calculated from it,
|
61
|
+
# if noen given. In the example above Person records will have attribute cars_ids which will be
|
62
|
+
# an array populated with foreign keys.
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# Options, all optional:
|
66
|
+
#
|
67
|
+
# <tt>class_name</tt>:: Class name is calculated from name, but can be overridden here.
|
68
|
+
# <tt>foreign_key</tt>:: Foreign key is calculated from name suffixed by _ids as default.
|
69
|
+
# <tt>store_in</tt>:: Send in the column family to store foreign key in. If none given,
|
70
|
+
# you should define the foreign key method in class if it can be
|
71
|
+
# calculated from another attributes in your class.
|
72
|
+
# <tt>records_starts_from</tt>:: A method name which returns an ID to start from when fetching rows in
|
73
|
+
# Person's table. This is useful if you for instance has a person with id 1
|
74
|
+
# and in your table for cars have cars id like "<person_id>-<incremental number>"
|
75
|
+
# or something. Then you can say references_many :cars, :starts_with => :id.
|
76
|
+
# <tt>find_with</tt>:: Assign it to a Proc and we will call it with the proxy_owner if you need complete
|
77
|
+
# control over how you retrieve your record.
|
78
|
+
# As a default TargetClass.find(foreign_keys_method) is used.
|
79
|
+
#
|
80
|
+
#
|
81
|
+
# Example usage:
|
82
|
+
#
|
83
|
+
# person = Person.first
|
84
|
+
# person.cars # loads and returns all cars.
|
85
|
+
# person.cars.first # Returns first car, either by loading just one object, or return first object in loaded proxy.
|
86
|
+
# person.cars.find("an_id") # Tries to load car with id 1 if that id is among person's cars. Either by a query and look among loaded records
|
87
|
+
# person.cars.limit(3) # Returns the 3 first cars, either by slice the loaded array of cars, or do a limited DB query.
|
88
|
+
#
|
89
|
+
#
|
90
|
+
def references_many(name, *args)
|
91
|
+
metadata = set_up_relation('references_many', name, *args)
|
92
|
+
create_references_many_accessors(metadata)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def set_up_relation(type, name, *args)
|
98
|
+
ensure_relations_exists
|
99
|
+
|
100
|
+
Metadata.new(name, *args).tap do |metadata|
|
101
|
+
metadata.relation_type = type
|
102
|
+
raise RelationAlreadyDefined unless self.relations.add?(metadata)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def ensure_relations_exists
|
107
|
+
self.relations = Set.new if relations.nil?
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def create_references_one_accessors(metadata)
|
112
|
+
redefine_method(metadata.name) do
|
113
|
+
proxy = relation_proxy(metadata.name)
|
114
|
+
proxy.load_proxy_target ? proxy : nil
|
115
|
+
end
|
116
|
+
|
117
|
+
redefine_method(metadata.name+'=') do |record|
|
118
|
+
relation_proxy(metadata.name).replace(record)
|
119
|
+
end
|
120
|
+
|
121
|
+
if metadata.persisting_foreign_key?
|
122
|
+
add_field_to_column_family(metadata.store_in, metadata.foreign_key)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_references_one_polymorphic_accessors(metadata)
|
127
|
+
if metadata.persisting_foreign_key?
|
128
|
+
add_field_to_column_family(metadata.store_in, metadata.polymorphic_type_column)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_references_many_accessors(metadata)
|
133
|
+
redefine_method(metadata.name) do
|
134
|
+
relation_proxy(metadata.name)
|
135
|
+
end
|
136
|
+
|
137
|
+
if metadata.persisting_foreign_key?
|
138
|
+
add_field_to_column_family(metadata.store_in, metadata.foreign_key, :type => :array, :default => [])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def relation_proxy(name)
|
148
|
+
name = name.to_s
|
149
|
+
|
150
|
+
unless proxy = relation_proxy_get(name)
|
151
|
+
if metadata = relations.find { |meta| meta.name == name }
|
152
|
+
proxy = metadata.new_relation_proxy(self)
|
153
|
+
relation_proxy_set(name, proxy)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
proxy
|
158
|
+
end
|
159
|
+
|
160
|
+
def relation_proxy_get(name)
|
161
|
+
@relation_proxy_cache[name.to_s]
|
162
|
+
end
|
163
|
+
|
164
|
+
def relation_proxy_set(name, proxy)
|
165
|
+
@relation_proxy_cache[name.to_s] = proxy
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|