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.
Files changed (83) hide show
  1. data/CHANGELOG.md +28 -5
  2. data/Gemfile.lock +12 -12
  3. data/README.md +29 -1
  4. data/lib/massive_record/adapters/initialize.rb +18 -0
  5. data/lib/massive_record/adapters/thrift/adapter.rb +25 -0
  6. data/lib/massive_record/adapters/thrift/column_family.rb +24 -0
  7. data/lib/massive_record/adapters/thrift/connection.rb +73 -0
  8. data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase.rb +0 -0
  9. data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase_constants.rb +0 -0
  10. data/lib/massive_record/{thrift → adapters/thrift/hbase}/hbase_types.rb +0 -0
  11. data/lib/massive_record/adapters/thrift/row.rb +150 -0
  12. data/lib/massive_record/adapters/thrift/scanner.rb +59 -0
  13. data/lib/massive_record/adapters/thrift/table.rb +169 -0
  14. data/lib/massive_record/orm/attribute_methods/read.rb +2 -1
  15. data/lib/massive_record/orm/base.rb +61 -3
  16. data/lib/massive_record/orm/coders/chained.rb +71 -0
  17. data/lib/massive_record/orm/coders/json.rb +17 -0
  18. data/lib/massive_record/orm/coders/yaml.rb +15 -0
  19. data/lib/massive_record/orm/coders.rb +3 -0
  20. data/lib/massive_record/orm/errors.rb +15 -2
  21. data/lib/massive_record/orm/finders/scope.rb +166 -0
  22. data/lib/massive_record/orm/finders.rb +45 -24
  23. data/lib/massive_record/orm/persistence.rb +4 -4
  24. data/lib/massive_record/orm/relations/interface.rb +170 -0
  25. data/lib/massive_record/orm/relations/metadata.rb +150 -0
  26. data/lib/massive_record/orm/relations/proxy/references_many.rb +229 -0
  27. data/lib/massive_record/orm/relations/proxy/references_one.rb +40 -0
  28. data/lib/massive_record/orm/relations/proxy/references_one_polymorphic.rb +49 -0
  29. data/lib/massive_record/orm/relations/proxy.rb +174 -0
  30. data/lib/massive_record/orm/relations.rb +6 -0
  31. data/lib/massive_record/orm/schema/column_interface.rb +1 -1
  32. data/lib/massive_record/orm/schema/field.rb +62 -27
  33. data/lib/massive_record/orm/single_table_inheritance.rb +21 -0
  34. data/lib/massive_record/version.rb +1 -1
  35. data/lib/massive_record/wrapper/adapter.rb +6 -0
  36. data/lib/massive_record/wrapper/base.rb +6 -7
  37. data/lib/massive_record/wrapper/cell.rb +9 -32
  38. data/lib/massive_record/wrapper/column_families_collection.rb +2 -2
  39. data/lib/massive_record/wrapper/errors.rb +10 -0
  40. data/lib/massive_record/wrapper/tables_collection.rb +1 -1
  41. data/lib/massive_record.rb +5 -12
  42. data/spec/orm/cases/attribute_methods_spec.rb +5 -1
  43. data/spec/orm/cases/base_spec.rb +77 -4
  44. data/spec/orm/cases/column_spec.rb +1 -1
  45. data/spec/orm/cases/finder_default_scope.rb +53 -0
  46. data/spec/orm/cases/finder_scope_spec.rb +288 -0
  47. data/spec/orm/cases/finders_spec.rb +56 -13
  48. data/spec/orm/cases/persistence_spec.rb +20 -5
  49. data/spec/orm/cases/single_table_inheritance_spec.rb +26 -0
  50. data/spec/orm/cases/table_spec.rb +1 -1
  51. data/spec/orm/cases/timestamps_spec.rb +16 -16
  52. data/spec/orm/coders/chained_spec.rb +73 -0
  53. data/spec/orm/coders/json_spec.rb +6 -0
  54. data/spec/orm/coders/yaml_spec.rb +6 -0
  55. data/spec/orm/models/best_friend.rb +7 -0
  56. data/spec/orm/models/friend.rb +4 -0
  57. data/spec/orm/models/person.rb +20 -6
  58. data/spec/orm/models/{person_with_timestamps.rb → person_with_timestamp.rb} +1 -1
  59. data/spec/orm/models/test_class.rb +3 -0
  60. data/spec/orm/relations/interface_spec.rb +207 -0
  61. data/spec/orm/relations/metadata_spec.rb +202 -0
  62. data/spec/orm/relations/proxy/references_many_spec.rb +624 -0
  63. data/spec/orm/relations/proxy/references_one_polymorphic_spec.rb +106 -0
  64. data/spec/orm/relations/proxy/references_one_spec.rb +111 -0
  65. data/spec/orm/relations/proxy_spec.rb +13 -0
  66. data/spec/orm/schema/field_spec.rb +101 -2
  67. data/spec/shared/orm/coders/an_orm_coder.rb +14 -0
  68. data/spec/shared/orm/relations/proxy.rb +154 -0
  69. data/spec/shared/orm/relations/singular_proxy.rb +68 -0
  70. data/spec/spec_helper.rb +1 -0
  71. data/spec/thrift/cases/encoding_spec.rb +28 -7
  72. data/spec/wrapper/cases/adapter_spec.rb +9 -0
  73. data/spec/wrapper/cases/connection_spec.rb +13 -10
  74. data/spec/wrapper/cases/table_spec.rb +85 -85
  75. metadata +74 -22
  76. data/TODO.md +0 -8
  77. data/lib/massive_record/exceptions.rb +0 -11
  78. data/lib/massive_record/wrapper/column_family.rb +0 -22
  79. data/lib/massive_record/wrapper/connection.rb +0 -71
  80. data/lib/massive_record/wrapper/row.rb +0 -173
  81. data/lib/massive_record/wrapper/scanner.rb +0 -61
  82. data/lib/massive_record/wrapper/table.rb +0 -149
  83. 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
@@ -0,0 +1,15 @@
1
+ module MassiveRecord
2
+ module ORM
3
+ module Coders
4
+ class YAML
5
+ def dump(object)
6
+ ::YAML.dump(object)
7
+ end
8
+
9
+ def load(yaml)
10
+ ::YAML.load(yaml)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ require 'massive_record/orm/coders/json'
2
+ require 'massive_record/orm/coders/yaml'
3
+ require 'massive_record/orm/coders/chained'
@@ -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 ? [] : nil) unless table.exists?
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.deserialize_value
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
- allocate.tap do |model|
119
- model.init_with('attributes' => record)
120
- end
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