Class: Hold::Sequel::PropertyMapper::ManyToMany

Inherits:
Hold::Sequel::PropertyMapper show all
Defined in:
lib/hold/sequel/property_mapper/many_to_many.rb

Overview

Maps to an array of associated objects stored in another repo, where a :join_table exists with columns for:

- our id property (:left_key)
- other repo's id property (:right_key)
- order position within the list, starting from 0 (:order_column)

By default these properties aren’t writeable - when they are writeable:

(for now at least) the rows of the join table are owned and managed soley by the parent objects via this mapper. The associated objects themselves, however, are free-floating and are not touched during create/update/delete (except optionally to store_new any new ones on create of the parent object, when :auto_store_new => true).

If you supply a hash as :filter, this will be used to filter the join table, and will also be merged into any rows inserted into the join table. So if you use it on a writeable property, it needs to be map columns just to values rather than to other sql conditions.

NB: for now this does not assume (or do anything special with respect to) the presence of a reciprocal

many-to_many property on the target repo. This functionality will need adding later to help figure out
the side-effects of changes to a many-to-many property when it comes to cache invalidation, and to
ensure that the order given by the order_column is not upset by updates to the corresponding reciprocal
property.

So:

- Rows are inserted into the join table after the parent object is created
- Rows in the join table are nuked and re-inserted after this property on the parent object is updated
- Rows in the join table are 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)

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

#build_insert_row, #build_update_row, #columns_aliases_and_tables_for_select, #make_filter, #make_multi_filter, #post_delete, #pre_insert, #pre_update

Constructor Details

#initialize(repo, property_name, options, &block) ⇒ ManyToMany

Returns a new instance of ManyToMany.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 40

def initialize(repo, property_name, options, &block)
  super(repo, property_name, &nil)

  @join_table = options[:join_table] || :"#{repo.main_table}_#{property_name}"
  @left_key   = options[:left_key]   || :"#{repo.main_table.to_s.singularize}_id"
  @right_key  = options[:right_key]  || :"#{property_name.to_s.singularize}_id"
  @qualified_left_key = Sequel::SQL::QualifiedIdentifier.new(@join_table, @left_key)
  @qualified_right_key = Sequel::SQL::QualifiedIdentifier.new(@join_table, @right_key)

  @filter = options[:filter]
  @join_table_dataset = @repository.db[@join_table]
  @distinct = options[:distinct] || false

  @order_column = options[:order_column] and
    @qualified_order_column = Sequel::SQL::QualifiedIdentifier.new(@join_table, @order_column)

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

  @model_class = options[:model_class] or raise ArgumentError

  # in case you want to override anything on the instance:
  instance_eval(&block) if block
end

Instance Attribute Details

#auto_store_newObject (readonly)

Returns the value of attribute auto_store_new.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def auto_store_new
  @auto_store_new
end

#distinctObject (readonly)

Returns the value of attribute distinct.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def distinct
  @distinct
end

#filterObject (readonly)

Returns the value of attribute filter.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def filter
  @filter
end

#join_tableObject (readonly)

Returns the value of attribute join_table.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def join_table
  @join_table
end

#left_keyObject (readonly)

Returns the value of attribute left_key.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def left_key
  @left_key
end

#manual_cascade_deleteObject (readonly)

Returns the value of attribute manual_cascade_delete.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def manual_cascade_delete
  @manual_cascade_delete
end

#model_classObject (readonly)

Returns the value of attribute model_class.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def model_class
  @model_class
end

#order_columnObject (readonly)

Returns the value of attribute order_column.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def order_column
  @order_column
end

#right_keyObject (readonly)

Returns the value of attribute right_key.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def right_key
  @right_key
end

#target_repoObject

Returns the value of attribute target_repo.



35
36
37
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 35

def target_repo
  @target_repo
end

#writeableObject (readonly)

Returns the value of attribute writeable.



37
38
39
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 37

def writeable
  @writeable
end

Class Method Details

.setter_dependencies_for(options = {}) ⇒ Object



30
31
32
33
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 30

def self.setter_dependencies_for(options={})
  features = [*options[:model_class]].map {|klass| [:get_class, klass]}
  {:target_repo => [IdentitySetRepository, *features]}
end

Instance Method Details

#add_denormalized_columns_to_join_table_row(entity, value, row) ⇒ Object

this is a hook for you to override



133
134
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 133

def add_denormalized_columns_to_join_table_row(entity, value, row)
end

#delete_join_table_rows(id) ⇒ Object



136
137
138
139
140
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 136

def delete_join_table_rows(id)
  filters = {@left_key => id}
  filters.merge!(@filter) if @filter
  @join_table_dataset.filter(filters).delete
end

#get_many_by_member(member) ⇒ Object

find all instances in this repo whose value for this property contains the given member instance



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 101

def get_many_by_member(member)
  @repository.query do |dataset, property_columns|
    id_column = property_columns[@repository.identity_property].first
    dataset = dataset.
      join(@join_table, @qualified_left_key => id_column).
      filter(@qualified_right_key => member.id)
    dataset = dataset.filter(@filter) if @filter
    dataset = dataset.distinct if @distinct
    dataset
  end.to_a
end

#insert_join_table_rows(entity, id, values) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 115

def insert_join_table_rows(entity, id, values)
  rows = []
  values.each_with_index do |value, index|
    value_id = value.id || if @auto_store_new
      target_repo.store_new(value); value.id
    else
      raise "value for ManyToMany mapped property #{@property_name} has no id, and :auto_store_new not specified"
    end
    row = {@left_key => id, @right_key => value_id}
    row[@order_column] = index if @order_column
    row.merge!(@filter) if @filter
    add_denormalized_columns_to_join_table_row(entity, value, row)
    rows << row
  end
  @join_table_dataset.multi_insert(rows)
end

#load_value(row, id, properties = nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 66

def load_value(row, id, properties=nil)
  target_repo.query(properties) do |dataset, property_columns|
    id_column = property_columns[target_repo.identity_property].first
    dataset = dataset.
      join(@join_table, @qualified_right_key => id_column).
      filter(@qualified_left_key => id)
    dataset = dataset.filter(@filter) if @filter
    dataset = dataset.distinct if @distinct
    @qualified_order_column ? dataset.order(@qualified_order_column) : dataset
  end.to_a
end

#load_values(rows, ids = nil, properties = nil, &b) ⇒ Object

efficient batch load for the non-lazy case



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 79

def load_values(rows, ids=nil, properties=nil, &b)
  query = target_repo.query(properties) do |dataset, mapping|
    id_column = mapping[target_repo.identity_property]
    dataset = dataset
      .join(@join_table, @qualified_right_key => id_column)
      .filter(@qualified_left_key => ids)
      .select(Sequel.as(@qualified_left_key,:_many_to_many_id))
    dataset = dataset.filter(@filter) if @filter
    dataset = dataset.distinct if @distinct
    dataset = dataset.order(:_many_to_many_id, @qualified_order_column) if @qualified_order_column
    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[:_many_to_many_id]] << entity
  end
  groups.each_with_index(&b)
end

#post_insert(entity, rows, insert_id) ⇒ Object



142
143
144
145
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 142

def post_insert(entity, rows, insert_id)
  return unless @writeable
  values = entity[@property_name] and insert_join_table_rows(entity, insert_id, values)
end

#post_update(entity, update_entity, rows, result_from_pre_update = nil) ⇒ Object



147
148
149
150
151
152
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 147

def post_update(entity, update_entity, rows, result_from_pre_update=nil)
  return unless @writeable
  update_values = update_entity[@property_name] or return
  delete_join_table_rows(entity.id)
  insert_join_table_rows(entity, entity.id, update_values)
end

#pre_delete(entity) ⇒ Object



154
155
156
# File 'lib/hold/sequel/property_mapper/many_to_many.rb', line 154

def pre_delete(entity)
  delete_join_table_rows(entity.id) if @manual_cascade_delete
end