Class: Hold::Sequel::PropertyMapper::OneToMany
- Inherits:
-
Hold::Sequel::PropertyMapper
- Object
- Hold::Sequel::PropertyMapper
- Hold::Sequel::PropertyMapper::OneToMany
- Defined in:
- lib/hold/sequel/property_mapper/one_to_many.rb
Overview
Maps to an array of associated objects stored in another repo, which has a foreign_key-mapped property pointing at instances of our model class.
By default these properties aren’t writeable - when they are writeable, the values are treated like wholy-owned sub-components of the parent object.
So, objects which are values of this property are:
- Created after the parent object is created
- Created/updated/deleted as appropriate after this property on the parent object is updated
- Deleted before the parent object is deleted (unless :manual_cascade_delete => false is
specified hinting that ON CASCADE DELETE is set on the foreign key so we needn't bother)
On update: We allow you to re-order and/or update the existing values while maintaining their identities, remove some objects which were in the collection before (which get deleted) and possibly throw in new objects too (which get created), but you can’t throw something in there which was previously attached to some other object, for the same reason that this doesn’t fly on insert.
If you specify a denormalized_count_column, this will be used to store the count of associated objects on a column on the main table of the parent object.
Instance Attribute Summary collapse
-
#denormalized_count_column ⇒ Object
readonly
Returns the value of attribute denormalized_count_column.
-
#foreign_key_property_name ⇒ Object
readonly
Returns the value of attribute foreign_key_property_name.
-
#manual_cascade_delete ⇒ Object
readonly
Returns the value of attribute manual_cascade_delete.
-
#model_class ⇒ Object
readonly
Returns the value of attribute model_class.
-
#order_direction ⇒ Object
readonly
Returns the value of attribute order_direction.
-
#order_property ⇒ Object
readonly
Returns the value of attribute order_property.
-
#target_repo ⇒ Object
Returns the value of attribute target_repo.
-
#writeable ⇒ Object
readonly
Returns the value of attribute writeable.
Attributes inherited from Hold::Sequel::PropertyMapper
#property, #property_name, #repository
Class Method Summary collapse
Instance Method Summary collapse
-
#add_join(dataset) ⇒ Object
adds a join to the target_repo’s table, onto a dataset from the mapper’s repository.
- #build_insert_row(entity, table, row, id = nil) ⇒ Object
- #build_update_row(entity, table, row, id = nil) ⇒ Object
- #foreign_key_mapper ⇒ Object
-
#get_many_by_member(member) ⇒ Object
help the parent repo find instances whose value for this property contains a particular member.
-
#initialize(repo, property_name, options) ⇒ OneToMany
constructor
A new instance of OneToMany.
- #load_value(row, id, properties = nil) ⇒ Object
- #load_values(rows, ids = nil, properties = nil, &b) ⇒ Object
- #post_insert(entity, rows, insert_id) ⇒ Object
- #post_update(entity, update_entity, rows, values_before) ⇒ Object
- #pre_delete(entity) ⇒ Object
- #pre_update(entity, update_entity) ⇒ Object
- #set_foreign_key_and_order_properties_on_value(entity, value, index) ⇒ Object
Methods inherited from Hold::Sequel::PropertyMapper
#columns_aliases_and_tables_for_select, #make_filter, #make_multi_filter, #post_delete, #pre_insert
Constructor Details
#initialize(repo, property_name, options) ⇒ OneToMany
Returns a new instance of OneToMany.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 33 def initialize(repo, property_name, ) super(repo, property_name) @foreign_key_property_name = [:property] or raise "missing :property arg" @order_property = [:order_property] @order_direction = [:order_direction] || :asc @extra_properties = {@foreign_key_property_name => true} @extra_properties[@order_property] = true if @order_property @writeable = [:writeable] || false @manual_cascade_delete = [:manual_cascade_delete] != false @denormalized_count_column = [:denormalized_count_column] @model_class = [:model_class] or raise ArgumentError end |
Instance Attribute Details
#denormalized_count_column ⇒ Object (readonly)
Returns the value of attribute denormalized_count_column.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def denormalized_count_column @denormalized_count_column end |
#foreign_key_property_name ⇒ Object (readonly)
Returns the value of attribute foreign_key_property_name.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def foreign_key_property_name @foreign_key_property_name end |
#manual_cascade_delete ⇒ Object (readonly)
Returns the value of attribute manual_cascade_delete.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def manual_cascade_delete @manual_cascade_delete end |
#model_class ⇒ Object (readonly)
Returns the value of attribute model_class.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def model_class @model_class end |
#order_direction ⇒ Object (readonly)
Returns the value of attribute order_direction.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def order_direction @order_direction end |
#order_property ⇒ Object (readonly)
Returns the value of attribute order_property.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def order_property @order_property end |
#target_repo ⇒ Object
Returns the value of attribute target_repo.
28 29 30 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 28 def target_repo @target_repo end |
#writeable ⇒ Object (readonly)
Returns the value of attribute writeable.
30 31 32 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 30 def writeable @writeable end |
Class Method Details
.setter_dependencies_for(options = {}) ⇒ Object
23 24 25 26 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 23 def self.setter_dependencies_for(={}) features = [*[:model_class]].map {|klass| [:get_class, klass]} {:target_repo => [IdentitySetRepository, *features]} end |
Instance Method Details
#add_join(dataset) ⇒ Object
adds a join to the target_repo’s table, onto a dataset from the mapper’s repository.
99 100 101 102 103 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 99 def add_join(dataset) # FIXME: doesn't take any care to pick a unique alias for the table when joining to it # FIXME: doesn't use mapping to determine id_column dataset.join(target_repo.table_name, foreign_key_mapper.column_name => @repository.identity_mapper.column_name) end |
#build_insert_row(entity, table, row, id = nil) ⇒ Object
120 121 122 123 124 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 120 def build_insert_row(entity, table, row, id=nil) return unless @denormalized_count_column && table == @repository.main_table values = entity[@property_name] row[@denormalized_count_column] = (values ? values.length : 0) end |
#build_update_row(entity, table, row, id = nil) ⇒ Object
173 174 175 176 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 173 def build_update_row(entity, table, row, id=nil) return unless @denormalized_count_column && table == @repository.main_table values = entity[@property_name] and row[@denormalized_count_column] = values.length end |
#foreign_key_mapper ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 51 def foreign_key_mapper @foreign_key_mapper ||= begin mapper = target_repo.mapper(@foreign_key_property_name) unless mapper.is_a?(PropertyMapper::ForeignKey) raise "OneToManyMapper: Expected ForeignKey mapper with name #{@foreign_key_property_name}" end unless mapper.target_repo.can_get_class?(@repository.model_class) raise "OneToManyMapper: ForeignKey mapper's target repo #{mapper.target_repo.inspect} can't get our repository's model_class #{@repository.model_class}" end mapper end end |
#get_many_by_member(member) ⇒ Object
help the parent repo find instances whose value for this property contains a particular member. since we’re one-to-many rather than many-to-many, this is relatively simple. we just get the foreign key property on the proposed member, see if it’s set to anything, and if so if that thing exists within our repo. if it does then it’s the only such object, because the member’s foreign key can only point at one thing.
109 110 111 112 113 114 115 116 117 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 109 def get_many_by_member(member) if member.has_key?(@foreign_key_property_name) object = member[@foreign_key_property_name] [object] if object && @repository.contains?(object) # we might not actually contain it else object = target_repo.get_property(member, @foreign_key_property_name) [object] if object # we know we contain it since the target_repo's foreign_key_mapper has us as its target_repo end end |
#load_value(row, id, properties = nil) ⇒ Object
64 65 66 67 68 69 70 71 72 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 64 def load_value(row, id, properties=nil) properties = (properties || target_repo.default_properties).merge(@extra_properties) target_repo.query(properties) do |dataset, mapping| filter = foreign_key_mapper.make_filter_by_id(id, mapping[@foreign_key_property_name]) dataset = dataset.filter(filter) dataset = dataset.order(Sequel.send(@order_direction, @order_property)) if @order_property dataset end.to_a end |
#load_values(rows, ids = nil, properties = nil, &b) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 74 def load_values(rows, ids=nil, properties=nil, &b) properties = (properties || target_repo.default_properties).merge(@extra_properties) query = target_repo.query(properties) do |dataset, mapping| filter = foreign_key_mapper.make_filter_by_ids(ids, mapping[@foreign_key_property_name]) dataset = dataset .filter(filter) .select(foreign_key_mapper.column_qualified.as(:_one_to_many_id)) if @order_property dataset = dataset.order(:_one_to_many_id, target_repo.mapper(@order_property).column_qualified.send(@order_direction)) end dataset end groups = []; id_to_group = {} ids.each_with_index {|id,index| id_to_group[id] = groups[index] = []} query.results_with_rows.each do |entity, row| id_to_group[row[:_one_to_many_id]] << entity end groups.each_with_index(&b) end |
#post_insert(entity, rows, insert_id) ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 126 def post_insert(entity, rows, insert_id) return unless @writeable values = entity[@property_name] or return # save the assocatied objects! values.each_with_index do |value, index| # if we allowed this you would potentially be detaching the object from its previous parent, # but then we'd have to apply hooks etc to that object too, so rather avoid: raise "OneToMany mapper for #{@property_name}: already-persisted values are not supported on insert" if value.id set_foreign_key_and_order_properties_on_value(entity, value, index) target_repo.store_new(value) end end |
#post_update(entity, update_entity, rows, values_before) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 178 def post_update(entity, update_entity, rows, values_before) return unless @writeable update_values = update_entity[@property_name] or return # delete any values which are no longer around: (values_before - update_values).each {|value| target_repo.delete(value)} # insert any new ones / update any existing ones which remain: update_values.each_with_index do |value, index| raise "OneToMany mapper: already-persisted values are only allowed for property update where they \ were already a value of the property beforehand" if value.id && !values_before.include?(value) set_foreign_key_and_order_properties_on_value(entity, value, index) # this will insert any new values, or update any existing ones. target_repo.store(value) end end |
#pre_delete(entity) ⇒ Object
194 195 196 197 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 194 def pre_delete(entity) return unless @manual_cascade_delete load_value(nil, entity.id).each {|value| target_repo.delete(value)} end |
#pre_update(entity, update_entity) ⇒ Object
168 169 170 171 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 168 def pre_update(entity, update_entity) # if an update is specified for this property, find out what the existing values are first: load_value(nil, entity.id) if @writeable && update_entity[@property_name] end |
#set_foreign_key_and_order_properties_on_value(entity, value, index) ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/hold/sequel/property_mapper/one_to_many.rb', line 141 def set_foreign_key_and_order_properties_on_value(entity, value, index) # ensure their corresponding foreign key mapped property points back at us if (existing_value = value[@foreign_key_property_name]) # the associated object has a foreign key mapped property pointing at something else. # # we could have config to allow it to go and update the foreign key in cases like this, but could # be messy in the presence of order columns etc. raise "OneToMany mapper: one of the values for mapped property #{@property_name} has an existing \ value for the corresponding #{@foreign_key_property_name} property which is not equal \ to our good selves" unless existing_value == entity else value[@foreign_key_property_name] = entity end # ensure their order_property corresponds to their order in the array, at least for new members. # (in an update, existing members may change order) if @order_property == :position if !value.id && (existing_index = value[@order_property]) raise "OneToMany mapper: one of the new values for mapped property #{@property_name} has an existing \ value for the order property #{@order_property} property which is not equal to its index in \ the array" unless existing_index == index else value[@order_property] = index end end end |