Class: Hold::Sequel::PropertyMapper::OneToMany

Inherits:
Hold::Sequel::PropertyMapper show all
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

Attributes inherited from Hold::Sequel::PropertyMapper

#property, #property_name, #repository

Class Method Summary collapse

Instance Method Summary collapse

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, options)
  super(repo, property_name)

  @foreign_key_property_name = options[:property] or raise "missing :property arg"
  @order_property = options[:order_property]
  @order_direction = options[:order_direction] || :asc

  @extra_properties = {@foreign_key_property_name => true}
  @extra_properties[@order_property] = true if @order_property

  @writeable = options[:writeable] || false
  @manual_cascade_delete = options[:manual_cascade_delete] != false

  @denormalized_count_column = options[:denormalized_count_column]

  @model_class = options[:model_class] or raise ArgumentError
end

Instance Attribute Details

#denormalized_count_columnObject (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_nameObject (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_deleteObject (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_classObject (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_directionObject (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_propertyObject (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_repoObject

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

#writeableObject (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(options={})
  features = [*options[: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_mapperObject



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