Class: Hold::Sequel::IdentitySetRepository

Inherits:
Object
  • Object
show all
Extended by:
Wirer::Factory::Interface
Includes:
IdentitySetRepository
Defined in:
lib/hold/sequel/identity_set_repository.rb,
lib/hold/sequel/with_polymorphic_type_column.rb

Direct Known Subclasses

WithPolymorphicTypeColumn

Defined Under Namespace

Classes: WithPolymorphicTypeColumn

Constant Summary collapse

JUST_ID =
[:id].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from IdentitySetRepository

#cell, #id_cell, #load, #reload

Constructor Details

#initialize(db) ⇒ IdentitySetRepository

Returns a new instance of IdentitySetRepository.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/hold/sequel/identity_set_repository.rb', line 105

def initialize(db)
  raise "abstract superclass" if instance_of?(IdentitySetRepository)
  @db = db

  @tables = []
  @tables_id_columns = {}
  self.class.tables.each do |name,options|
    @tables << name
    @tables_id_columns[name] = options[:id_column]
    @id_sequence_table = name if options[:id_sequence]
    @main_table = name if options[:default]
  end
  @main_table ||= @tables.first

  @property_mappers = {}
  @default_properties = {}

  # map the identity_property
  @identity_property = :id # todo make this configurable
  @identity_mapper = @property_mappers[@identity_property] = PropertyMapper::Identity.new(self, @identity_property)

  self.class.property_mapper_args.each do |property_name, mapper_class, options, block|
    @property_mappers[property_name] = mapper_class.new(self, property_name, options, &block)
    @default_properties[property_name] = true if mapper_class <= PropertyMapper::Column
    # for foreign key properties, by default we only load the ID (which is already present on the parent result row):
    @default_properties[property_name] = JUST_ID if mapper_class <= PropertyMapper::ForeignKey
  end

  @property_mappers.freeze
end

Instance Attribute Details

#dbObject (readonly)

Returns the value of attribute db.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def db
  @db
end

#default_propertiesObject (readonly)

Returns the value of attribute default_properties.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def default_properties
  @default_properties
end

#id_sequence_tableObject (readonly)

Returns the value of attribute id_sequence_table.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def id_sequence_table
  @id_sequence_table
end

#identity_mapperObject (readonly)

Returns the value of attribute identity_mapper.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def identity_mapper
  @identity_mapper
end

#identity_propertyObject (readonly)

Returns the value of attribute identity_property.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def identity_property
  @identity_property
end

#main_tableObject (readonly)

Returns the value of attribute main_table.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def main_table
  @main_table
end

#property_mappersObject (readonly)

Returns the value of attribute property_mappers.



102
103
104
# File 'lib/hold/sequel/identity_set_repository.rb', line 102

def property_mappers
  @property_mappers
end

Class Method Details

.constructor_dependenciesObject



30
31
32
# File 'lib/hold/sequel/identity_set_repository.rb', line 30

def constructor_dependencies
  {:database => Wirer::Dependency.new_from_args(Sequel::Database)}
end

.inject_dependency(instance, dep_name, value) ⇒ Object



60
61
62
63
64
65
66
67
# File 'lib/hold/sequel/identity_set_repository.rb', line 60

def inject_dependency(instance, dep_name, value)
  if dep_name == :observers
    value.each {|observer| instance.add_observer(observer)}
  else
    mapper_name, dep_name = dep_name.to_s.split('__', 2)
    instance.mapper(mapper_name.to_sym).send("#{dep_name}=", value)
  end
end

.model_classObject



16
17
18
# File 'lib/hold/sequel/identity_set_repository.rb', line 16

def model_class
  @model_class ||= (superclass.model_class if superclass < IdentitySetRepository)
end

.new_from_dependencies(deps, *p) ⇒ Object



34
35
36
# File 'lib/hold/sequel/identity_set_repository.rb', line 34

def new_from_dependencies(deps, *p)
  new(deps[:database], *p)
end

.property_mapper_argsObject



24
25
26
# File 'lib/hold/sequel/identity_set_repository.rb', line 24

def property_mapper_args
  @property_mapper_args ||= (superclass < IdentitySetRepository ? superclass.property_mapper_args.dup : [])
end

.provides_classObject



38
# File 'lib/hold/sequel/identity_set_repository.rb', line 38

def provides_class; self; end

.provides_featuresObject



40
41
42
# File 'lib/hold/sequel/identity_set_repository.rb', line 40

def provides_features
  [[:get_class, model_class]]
end

.setter_dependencies(instance = nil) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/hold/sequel/identity_set_repository.rb', line 44

def setter_dependencies(instance=nil)
  dependencies = {:observers => Wirer::Dependency.new(
    :module   => Hold::Sequel::RepositoryObserver,
    :features => [[:observes_repo_for_class, model_class]],
    :multiple => true,
    :optional => true
  )}
  property_mapper_args.each do |property_name, mapper_class, options, block|
    mapper_class.setter_dependencies_for(options, &block).each do |dep_name, dep_args|
      mapper_dep_name = :"#{property_name}__#{dep_name}"
      dependencies[mapper_dep_name] = Wirer::Dependency.new_from_arg_or_args_list(dep_args)
    end
  end
  dependencies
end

.tablesObject



20
21
22
# File 'lib/hold/sequel/identity_set_repository.rb', line 20

def tables
  @tables ||= (superclass < IdentitySetRepository ? superclass.tables.dup : [])
end

Instance Method Details

#add_observer(observer) ⇒ Object

see Hold::Sequel::RepositoryObserver for the interface you need to expose to be an observer here.

If you’re using Wirer to construct the repository, a better way to hook the repo up with observers is to add RepositoryObservers to the Wirer::Container and have them provide feature [:observes_repo_for_class, model_class]. They’ll then get picked up by our multiple setter_dependency and added as an observer just after construction.



163
164
165
166
# File 'lib/hold/sequel/identity_set_repository.rb', line 163

def add_observer(observer)
  @observers ||= []
  @observers << observer
end

#allocates_ids?Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/hold/sequel/identity_set_repository.rb', line 142

def allocates_ids?
  !!@id_sequence_table
end

#array_cell_for_dataset(&b) ⇒ Object

ArrayCells for top-level collections



548
549
550
# File 'lib/hold/sequel/identity_set_repository.rb', line 548

def array_cell_for_dataset(&b)
  QueryArrayCell.new(self, &b)
end

#can_construct_from_id_alone?(properties) ⇒ Boolean

this determines if an optimisation can be done whereby if only the ID property is requested to be loaded, the object(s) can be constructed directly from their ids without needing to be fetched from the database.

Returns:

  • (Boolean)


257
258
259
# File 'lib/hold/sequel/identity_set_repository.rb', line 257

def can_construct_from_id_alone?(properties)
  properties == JUST_ID
end

#can_get_class?(model_class) ⇒ Boolean

is this repository capable of loading instances of the given model class? repositories which support polymorhpic loading may override this.

Returns:

  • (Boolean)


148
149
150
# File 'lib/hold/sequel/identity_set_repository.rb', line 148

def can_get_class?(model_class)
  model_class == self.model_class
end

#can_set_class?(model_class) ⇒ Boolean

is this repository capable of storing instances of the given model class? repositories which support polymorhpic writes may override this.

Returns:

  • (Boolean)


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

def can_set_class?(model_class)
  model_class == self.model_class
end

#columns_aliases_and_tables_for_properties(properties) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/hold/sequel/identity_set_repository.rb', line 269

def columns_aliases_and_tables_for_properties(properties)
  columns_by_property = {}; aliased_columns = []; tables = []
  properties.each do |p|
    next if p == @identity_property # this gets special handling
    cs, as, ts = mapper(p).columns_aliases_and_tables_for_select
    columns_by_property[p] = cs
    aliased_columns.concat(as)
    tables.concat(ts)
  end
  tables.unshift(@main_table) if tables.delete(@main_table)

  # the identity mapper gets called last, so that it can get a hint about what
  # tables are already required for the other columns. (seeing as how an identity column
  # needs to be present on every table used for a given repo, it should never need to
  # add an extra table just in order to select the ID)
  id_cols, id_aliases, id_tables = @identity_mapper.columns_aliases_and_tables_for_select(tables.first || @main_table)
  columns_by_property[@identity_property] = id_cols
  aliased_columns.concat(id_aliases)
  tables.concat(id_tables)
  aliased_columns.uniq!; tables.uniq!
  return columns_by_property, aliased_columns, tables
end

#construct_entity(property_hash, row = nil) ⇒ Object



239
240
241
242
243
244
245
246
# File 'lib/hold/sequel/identity_set_repository.rb', line 239

def construct_entity(property_hash, row=nil)
  # new_skipping_checks is supported by ThinModels::Struct(::Typed) and skips any type checks or
  # attribute name checks on the supplied attributes.
  @model_class_new_method ||= model_class.respond_to?(:new_skipping_checks) ? :new_skipping_checks : :new
  model_class.send(@model_class_new_method, property_hash) do |model, property|
    get_property(model, property)
  end
end

#construct_entity_from_id(id) ⇒ Object



248
249
250
251
252
# File 'lib/hold/sequel/identity_set_repository.rb', line 248

def construct_entity_from_id(id)
  model_class.new(@identity_property => id) do |model, property|
    get_property(model, property)
  end
end

#contains?(entity) ⇒ Boolean

Returns:

  • (Boolean)


379
380
381
# File 'lib/hold/sequel/identity_set_repository.rb', line 379

def contains?(entity)
  id = entity.id and contains_id?(id)
end

#contains_id?(id) ⇒ Boolean

Returns:

  • (Boolean)


373
374
375
376
377
# File 'lib/hold/sequel/identity_set_repository.rb', line 373

def contains_id?(id)
  dataset = dataset_to_select_tables(@main_table)
  id_filter = @identity_mapper.make_filter(id, [@tables_id_columns[@main_table]])
  dataset.filter(id_filter).select(1).limit(1).single_value ? true : false
end

#count_datasetObject



552
553
554
555
556
# File 'lib/hold/sequel/identity_set_repository.rb', line 552

def count_dataset
  dataset = dataset_to_select_tables(@main_table)
  dataset = yield dataset if block_given?
  dataset.count
end

#dataset_to_select_tables(*tables) ⇒ Object



261
262
263
264
265
266
267
# File 'lib/hold/sequel/identity_set_repository.rb', line 261

def dataset_to_select_tables(*tables)
  main_table, *other_tables = tables
  main_id = @identity_mapper.qualified_column_name(main_table)
  other_tables.inject(@db[main_table]) do |dataset, table|
    dataset.join(table, @identity_mapper.qualified_column_name(table) => main_id)
  end
end

#delete(entity) ⇒ Object

deletes rows for this id in all tables of the repo.

note: order of deletes is important here if you have foreign key dependencies between the ID columns of the different tables; this goes in the reverse order to that used for inserts by store_new, which in turn is determined by the order of your use_table declarations



511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/hold/sequel/identity_set_repository.rb', line 511

def delete(entity)
  id = entity.id or raise Hold::MissingIdentity
  transaction do
    pre_delete(entity)
    @property_mappers.each do |name, mapper|
      mapper.pre_delete(entity)
    end
    @tables.reverse_each do |table|
      id_filter = @identity_mapper.make_filter(id, [@tables_id_columns[table]])
      @db[table].filter(id_filter).delete
    end
    @property_mappers.each do |name, mapper|
      mapper.post_delete(entity)
    end
    post_delete(entity)
  end
end

#delete_id(id) ⇒ Object



541
542
543
544
# File 'lib/hold/sequel/identity_set_repository.rb', line 541

def delete_id(id)
  entity = construct_entity(@identity_property => id)
  delete(entity)
end

#get_all(options = {}) ⇒ Object



310
311
312
# File 'lib/hold/sequel/identity_set_repository.rb', line 310

def get_all(options={})
  query(options[:properties]).to_a(options[:lazy])
end

#get_by_id(id, options = {}) ⇒ Object



336
337
338
339
340
341
342
343
344
# File 'lib/hold/sequel/identity_set_repository.rb', line 336

def get_by_id(id, options={})
  properties = options[:properties]
  return construct_entity_from_id(id) if can_construct_from_id_alone?(properties)

  query(properties) do |dataset, property_columns|
    filter = @identity_mapper.make_filter(id, property_columns[@identity_property])
    dataset.filter(filter)
  end.single_result
end

#get_by_property(property, value, options = {}) ⇒ Object



369
370
371
# File 'lib/hold/sequel/identity_set_repository.rb', line 369

def get_by_property(property, value, options={})
  get_many_by_property(property, value, options).first
end

#get_many_by_ids(ids, options = {}) ⇒ Object

multi-get via a single SELECT… WHERE id IN (1,2,3,4)



347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/hold/sequel/identity_set_repository.rb', line 347

def get_many_by_ids(ids, options={})
  properties = options[:properties]
  return ids.map {|id| construct_entity_from_id(id)} if can_construct_from_id_alone?(properties)

  results_by_id = {}
  results = query(options[:properties]) do |ds,mapping|
    id_filter = @identity_mapper.make_multi_filter(ids.uniq, mapping[@identity_property])
    ds.filter(id_filter)
  end.to_a(options[:lazy])
  results.each {|object| results_by_id[object.id] = object}
  ids.map {|id| results_by_id[id]}
end

#get_many_by_property(property, value, options = {}) ⇒ Object



360
361
362
363
364
365
366
367
# File 'lib/hold/sequel/identity_set_repository.rb', line 360

def get_many_by_property(property, value, options={})
  properties_to_fetch ||= @default_properties.dup
  properties_to_fetch[property] = true
  query(options[:properties]) do |dataset, property_columns|
    filter = mapper(property).make_filter(value, property_columns[property])
    dataset.filter(filter)
  end.to_a(options[:lazy])
end

#get_many_with_dataset(options = {}, &b) ⇒ Object

Can take a block which may add extra conditions, joins, order etc onto the relevant query.



306
307
308
# File 'lib/hold/sequel/identity_set_repository.rb', line 306

def get_many_with_dataset(options={}, &b)
  query(options[:properties], &b).to_a(options[:lazy])
end

#get_property(entity, property, options = {}) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/hold/sequel/identity_set_repository.rb', line 320

def get_property(entity, property, options={})
  unless property.is_a? Symbol
    fail ArgumentError, 'get_property must suppy a symbol'
  end
  begin
    result = query(property => options[:properties]) do |dataset, property_columns|
      filter = @identity_mapper.make_filter(entity.id, property_columns[@identity_property])
      dataset.filter(filter)
    end.single_result
  rescue TypeError
    # catches test errors caught by []ing a string post 1.8
    raise ArgumentError, 'get_property caught a type error, check options'
  end
  result && result[property]
end

#get_repo_dependencies_from(repo_set) ⇒ Object

if you want to avoid the need to manually pass in target_repo parameters for each property mapped by a foreign key mapper etc - this will have the mappers go find the dependency themselves.



176
177
178
# File 'lib/hold/sequel/identity_set_repository.rb', line 176

def get_repo_dependencies_from(repo_set)
  @property_mappers.each_value {|mapper| mapper.get_repo_dependencies_from(repo_set)}
end

#get_with_dataset(options = {}, &b) ⇒ Object

like get_many_with_dataset but just gets a single row, or nil if not found. adds limit(1) to the dataset for you.



316
317
318
# File 'lib/hold/sequel/identity_set_repository.rb', line 316

def get_with_dataset(options={}, &b)
  query(options[:properties], &b).single_result
end

#inspectObject



138
139
140
# File 'lib/hold/sequel/identity_set_repository.rb', line 138

def inspect
  "<##{self.class}: #{model_class}>"
end

#mapper(name) ⇒ Object

convenience to get a particular property mapper of this repo:

Raises:

  • (ArgumentError)


169
170
171
172
# File 'lib/hold/sequel/identity_set_repository.rb', line 169

def mapper(name)
  raise ArgumentError unless name.is_a?(Symbol)
  @property_mappers[name] or raise "#{self.class}: no such property mapper #{name.inspect}"
end

#model_classObject



100
# File 'lib/hold/sequel/identity_set_repository.rb', line 100

def model_class; self.class.model_class; end

#post_delete(entity) ⇒ Object

Remember to call super if you override this. If you do any extra deleting in an overridden post_delete, call super afterwards



537
538
539
# File 'lib/hold/sequel/identity_set_repository.rb', line 537

def post_delete(entity)
  @observers.each {|observer| observer.post_delete(self, entity)} if @observers
end

#post_insert(entity, rows, insert_id) ⇒ Object

Remember to call super if you override this. If you do any extra inserting in an overridden post_insert, call super afterwards



458
459
460
# File 'lib/hold/sequel/identity_set_repository.rb', line 458

def post_insert(entity, rows, insert_id)
  @observers.each {|observer| observer.post_insert(self, entity, rows, insert_id)} if @observers
end

#post_update(entity, update_entity, rows) ⇒ Object

Remember to call super if you override this. If you do any extra updating in an overridden post_update, call super afterwards



495
496
497
# File 'lib/hold/sequel/identity_set_repository.rb', line 495

def post_update(entity, update_entity, rows)
  @observers.each {|observer| observer.post_update(self, entity, update_entity, rows)} if @observers
end

#pre_delete(entity) ⇒ Object

Remember to call super if you override this. If you do any extra deleting in an overridden pre_delete, call super beforehand



531
532
533
# File 'lib/hold/sequel/identity_set_repository.rb', line 531

def pre_delete(entity)
  @observers.each {|observer| observer.pre_delete(self, entity)} if @observers
end

#pre_insert(entity) ⇒ Object

Remember to call super if you override this. If you do any extra inserting in an overridden pre_insert, call super beforehand



452
453
454
# File 'lib/hold/sequel/identity_set_repository.rb', line 452

def pre_insert(entity)
  @observers.each {|observer| observer.pre_insert(self, entity)} if @observers
end

#pre_update(entity, update_entity) ⇒ Object

Remember to call super if you override this. If you do any extra updating in an overridden pre_update, call super beforehand



489
490
491
# File 'lib/hold/sequel/identity_set_repository.rb', line 489

def pre_update(entity, update_entity)
  @observers.each {|observer| observer.pre_update(self, entity, update_entity)} if @observers
end

#query(properties = nil, &b) ⇒ Object

This is the main mechanism to retrieve stuff from the repo via custom queries.



299
300
301
302
# File 'lib/hold/sequel/identity_set_repository.rb', line 299

def query(properties=nil, &b)
  properties = @default_properties if properties == true || properties.nil?
  Query.new(self, properties, &b)
end

#store(entity) ⇒ Object

Calls one of store_new (insert) or update as appropriate.

Where the repo allocates_ids, you can supply an entity without an ID and store_new will be called.

If the entity has an ID, it will check whether it’s currently contained in the repository before calling store_new or update as appropriate.



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/hold/sequel/identity_set_repository.rb', line 392

def store(entity)
  id = entity.id
  if id
    transaction do
      if contains_id?(id)
        update(entity)
      else
        store_new(entity)
      end
    end
  else
    if allocates_ids?
      store_new(entity)
    else
      raise Hold::MissingIdentity
    end
  end
  entity
end

#store_new(entity) ⇒ Object

inserts rows into all relevant tables for the given entity. ensures that where one of the tables is used for an id sequence, that this row is inserted first and the resulting insert_id obtained is passed when building subsequent rows.

note: order of inserts is important here if you have foreign key dependencies between the ID columns of the different tables; if so you’ll need to order your use_table declarations accordingly.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/hold/sequel/identity_set_repository.rb', line 420

def store_new(entity)
  transaction do
    rows = {}; insert_id = nil
    pre_insert(entity)
    @property_mappers.each_value {|mapper| mapper.pre_insert(entity)}
    if @id_sequence_table
      row = insert_row_for_entity(entity, @id_sequence_table)
      insert_id = translate_exceptions {@db[@id_sequence_table].insert(row)}
      rows[@id_sequence_table] = row
    end
    # note: order is important here if you have foreign key dependencies, order
    # your use_table declarations appropriately:
    @tables.each do |table|
      next if table == @id_sequence_table # done that already
      row = insert_row_for_entity(entity, table, insert_id)
      translate_exceptions {@db[table].insert(row)}
      rows[table] = row
    end
    # identity_mapper should be called first, so that other mappers have the new ID
    # available on the entity when called.
    @identity_mapper.post_insert(entity, rows, insert_id)
    @property_mappers.each_value do |mapper|
      next if mapper == @identity_mapper
      mapper.post_insert(entity, rows, insert_id)
    end
    post_insert(entity, rows, insert_id)
    entity
  end
end

#table_id_column(table) ⇒ Object



180
181
182
# File 'lib/hold/sequel/identity_set_repository.rb', line 180

def table_id_column(table)
  @tables_id_columns[table]
end

#transaction(*p, &b) ⇒ Object



292
293
294
# File 'lib/hold/sequel/identity_set_repository.rb', line 292

def transaction(*p, &b)
  @db.transaction(*p, &b)
end

#update(entity, update_entity = entity) ⇒ Object



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/hold/sequel/identity_set_repository.rb', line 462

def update(entity, update_entity=entity)
  id = entity.id or raise Hold::MissingIdentity
  transaction do
    rows = {}; data_from_mappers = {}
    pre_update(entity, update_entity)
    @property_mappers.each do |name, mapper|
      data_from_mappers[name] = mapper.pre_update(entity, update_entity)
    end
    @tables.each do |table|
      row = update_row_for_entity(id, update_entity, table)
      unless row.empty?
        id_filter = @identity_mapper.make_filter(id, [@tables_id_columns[table]])
        translate_exceptions {@db[table].filter(id_filter).update(row)}
      end
      rows[table] = row
    end
    @property_mappers.each do |name, mapper|
      mapper.post_update(entity, update_entity, rows, data_from_mappers[name])
    end
    post_update(entity, update_entity, rows)
    entity.merge!(update_entity) if entity.respond_to?(:merge!)
    entity
  end
end

#update_by_id(id, update_entity) ⇒ Object



499
500
501
502
# File 'lib/hold/sequel/identity_set_repository.rb', line 499

def update_by_id(id, update_entity)
  entity = construct_entity(@identity_property => id)
  update(entity, update_entity)
end