Class: Rod::Model

Inherits:
AbstractModel show all
Extended by:
Enumerable, Utils
Includes:
ActiveModel::Dirty, ActiveModel::Validations
Defined in:
lib/rod/model.rb

Overview

Abstract class representing a model entity. Each storable class has to derieve from Model.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

remove_file, remove_files, remove_files_but, report_progress

Methods inherited from AbstractModel

compatible?, difference

Constructor Details

#initialize(options = nil) ⇒ Model

If options is an integer it is the @rod_id of the object.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/rod/model.rb', line 20

def initialize(options=nil)
  @reference_updaters = []
  case options
  when Integer
    @rod_id = options
  when Hash
    @rod_id = 0
    initialize_fields
    options.each do |key,value|
      begin
        self.send("#{key}=",value)
      rescue NoMethodError
        raise RodException.new("There is no field or association with name #{key}!")
      end
    end
  when NilClass
    @rod_id = 0
    initialize_fields
  else
    raise InvalidArgument.new("initialize(options)",options)
  end
end

Instance Attribute Details

#reference_updatersObject (readonly)

A list of updaters that has to be notified when the rod_id of this object is defined. See ReferenceUpdater for details.



17
18
19
# File 'lib/rod/model.rb', line 17

def reference_updaters
  @reference_updaters
end

Class Method Details

.[](index) ⇒ Object

Returns n-th (index) object of this class stored in the database. This call is scope-checked.



167
168
169
170
171
172
173
# File 'lib/rod/model.rb', line 167

def self.[](index)
  begin
    get(index+1)
  rescue IndexError
    nil
  end
end

.countObject

Returns the number of objects of this class stored in the database.



145
146
147
148
149
150
151
# File 'lib/rod/model.rb', line 145

def self.count
  self_count = database.count(self)
  # This should be changed if all other featurs connected with
  # inheritence are implemented, especially #14
  #subclasses.inject(self_count){|sum,sub| sum + sub.count}
  self_count
end

.eachObject

Iterates over object of this class stored in the database.



154
155
156
157
158
159
160
161
162
163
# File 'lib/rod/model.rb', line 154

def self.each
  #TODO an exception if in wrong state?
  if block_given?
    self.count.times do |index|
      yield get(index+1)
    end
  else
    enum_for(:each)
  end
end

.fieldsObject

Returns the fields of this class.



370
371
372
373
374
375
376
# File 'lib/rod/model.rb', line 370

def self.fields
  if self == Rod::Model
    @fields ||= []
  else
    @fields ||= superclass.fields.map{|p| p.copy(self)}
  end
end

.find_by_rod_id(rod_id) ⇒ Object

Finder for rod_id.



362
363
364
365
366
367
# File 'lib/rod/model.rb', line 362

def self.find_by_rod_id(rod_id)
  if rod_id <= 0 || rod_id > self.count
    return nil
  end
  get(rod_id)
end

.generate_class(class_name, metadata) ⇒ Object

Generates the model class based on the metadata and places it in the module_instance or Object (default scope) if module is nil.



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/rod/model.rb', line 417

def self.generate_class(class_name,)
  superclass = [:superclass].constantize
  namespace = define_context(class_name)
  klass = Class.new(superclass)
  namespace.const_set(class_name.split("::")[-1],klass)
  [:fields,:has_one,:has_many].each do |type|
    ([type] || []).each do |name,options|
      next if superclass.property(name)
      if type == :fields
        internal_options = options.dup
        field_type = internal_options.delete(:type)
        klass.send(:field,name,field_type,internal_options)
      else
        klass.send(type,name,options)
      end
    end
  end
  klass
end

.indexed_propertiesObject

Returns (and caches) only properties which are indexed.



799
800
801
# File 'lib/rod/model.rb', line 799

def indexed_properties
  @indexed_properties ||= self.properties.select{|p| p.options[:index]}
end

.metadataObject

Metadata for the model class.



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/rod/model.rb', line 397

def self.
  meta = super
  {:fields => :fields,
   :has_one => :singular_associations,
   :has_many => :plural_associations}.each do |type,method|
    # fields
     = {}
    self.send(method).each do |property|
      next if property.field? && property.identifier?
      [property.name] = property.
    end
    unless .empty?
      meta[type] = 
    end
  end
  meta
end

.migrateObject

Migrates the class to the new model, i.e. it copies all the values of properties that both belong to the class in the old and the new model; it initializes new properties with default values and migrates the indices to different implementations.



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/rod/model.rb', line 441

def self.migrate
  # check if the migration is needed
   = self.
  .merge!({:superclass => [:superclass].sub(LEGACY_RE,"")})
  new_class = self.name.sub(LEGACY_RE,"").constantize
  if new_class.compatible?()
    backup_path = self.path_for_data(database.path)
    new_path = new_class.path_for_data(database.path)
    puts "Copying #{backup_path} to #{new_path}" if $ROD_DEBUG
    FileUtils.cp(backup_path,new_path)
    new_class.indexed_properties.each do |property|
      backup_path = self.property(property.name).index.path
      new_path = property.index.path
      puts "Copying #{backup_path} to #{new_path}" if $ROD_DEBUG
      FileUtils.cp(backup_path,new_path) if File.exist?(backup_path)
    end
    return
  end
  database.send(:allocate_space,new_class)

  puts "Migrating #{new_class}" if $ROD_DEBUG
  # Check for incompatible properties.
  self.properties.each do |name,property|
    next unless new_class.property(name)
    difference = property.difference(new_class.properties[name])
    difference.delete(:index)
    # Check if there are some options which we cannot migrate at the
    # moment.
    unless difference.empty?
      raise IncompatibleVersion.
        new("Incompatible definition of property '#{name}'\n" +
            "Definition of '#{name}' is different in the old and "+
            "the new schema for '#{new_class}':\n" +
            "  #{difference}")
    end
  end
  # Migrate the objects.
  # initialize prototype objects
  old_object = self.new
  new_object = new_class.new
  self.properties.each do |property|
    # optimization
    name = property.name.to_s
    next unless new_class.property(name.to_sym)
    print "-  #{name}... " if $ROD_DEBUG
    if property.field?
      if property.variable_size?
        self.count.times do |position|
          new_object.send("_#{name}_length=",position+1,
                          old_object.send("_#{name}_length",position+1))
          new_object.send("_#{name}_offset=",position+1,
                          old_object.send("_#{name}_offset",position+1))
          report_progress(position,self.count) if $ROD_DEBUG
        end
      else
        self.count.times do |position|
          new_object.send("_#{name}=",position + 1,
                          old_object.send("_#{name}",position + 1))
          report_progress(position,self.count) if $ROD_DEBUG
        end
      end
    elsif property.singular?
      self.count.times do |position|
        new_object.send("_#{name}=",position + 1,
                        old_object.send("_#{name}",position + 1))
        report_progress(position,self.count) if $ROD_DEBUG
      end
      if property.polymorphic?
        self.count.times do |position|
          new_object.send("_#{name}__class=",position + 1,
                          old_object.send("_#{name}__class",position + 1))
          report_progress(position,self.count) if $ROD_DEBUG
        end
      end
    else
      self.count.times do |position|
        new_object.send("_#{name}_count=",position + 1,
                        old_object.send("_#{name}_count",position + 1))
        new_object.send("_#{name}_offset=",position + 1,
                        old_object.send("_#{name}_offset",position + 1))
        report_progress(position,self.count) if $ROD_DEBUG
      end
    end
    puts " done" if $ROD_DEBUG
  end
  # Migrate the indices.
  new_class.indexed_properties.each do |property|
    # Migrate to new options.
    old_index_type = self.property(property.name) && self.property(property.name).options[:index]
    if old_index_type.nil?
      print "-  building index #{property.options[:index]} for '#{property.name}'... " if $ROD_DEBUG
      new_class.rebuild_index(property)
      puts " done" if $ROD_DEBUG
    elsif property.options[:index] == old_index_type
      backup_path = self.property(property.name).index.path
      new_path = property.index.path
      puts "Copying #{backup_path} to #{new_path}" if $ROD_DEBUG
      FileUtils.cp(backup_path,new_path) if File.exist?(backup_path)
    else
      print "-  copying #{property.options[:index]} index for '#{property.name}'... " if $ROD_DEBUG
      new_index = property.index
      old_index = self.property(property.name).index
      new_index.copy(old_index)
      puts " done" if $ROD_DEBUG
    end
  end
end

.plural_associationsObject

Returns plural associations of this class.



388
389
390
391
392
393
394
# File 'lib/rod/model.rb', line 388

def self.plural_associations
  if self == Rod::Model
    @plural_associations ||= []
  else
    @plural_associations ||= superclass.plural_associations.map{|p| p.copy(self)}
  end
end

.propertiesObject

Fields, singular and plural associations.



789
790
791
# File 'lib/rod/model.rb', line 789

def properties
  @properties ||= self.fields + self.singular_associations + self.plural_associations
end

.property(name) ⇒ Object

Returns the property with given name or nil if it doesn’t exist.



794
795
796
# File 'lib/rod/model.rb', line 794

def property(name)
  properties.find{|p| p.name == name}
end

.singular_associationsObject

Returns singular associations of this class.



379
380
381
382
383
384
385
# File 'lib/rod/model.rb', line 379

def self.singular_associations
  if self == Rod::Model
    @singular_associations ||= []
  else
    @singular_associations ||= superclass.singular_associations.map{|p| p.copy(self)}
  end
end

.store(object) ⇒ Object

Stores given object in the database. The object must be an instance of this class.



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/rod/model.rb', line 310

def self.store(object)
  unless object.is_a?(self)
    raise RodException.new("Incompatible object class #{object.class}.")
  end
  stored_now = object.new?
  database.store(self,object)
  cache[object.rod_id] = object

  # update class indices
  indexed_properties.each do |property|
    # WARNING: singular and plural associations with nil as value are not indexed!
    # TODO #156 think over this constraint, write specs in persistence.feature
    if property.field? || property.singular?
      if stored_now || object.changes.has_key?(property.name.to_s)
        unless stored_now
          old_value = object.changes[property.name.to_s][0]
          property.index[old_value].delete(object)
        end
        new_value = object.send(property.name)
        if property.field? || new_value
          property.index[new_value] << object
        end
      end
    else
      # plural
      object.send(property.name).deleted.each do |deleted|
        property.index[deleted].delete(object) unless deleted.nil?
      end
      object.send(property.name).added.each do |added|
        property.index[added] << object unless added.nil?
      end
    end
  end
end

.struct_nameObject

The name of the C struct for this class.



346
347
348
349
350
351
352
353
354
# File 'lib/rod/model.rb', line 346

def self.struct_name
  return @struct_name unless @struct_name.nil?
  name = struct_name_for(self.to_s)
  unless name =~ /^\#/
    # not an anonymous class
    @struct_name = name
  end
  name
end

.struct_name_for(name) ⇒ Object

Returns the struct name for the class name.



357
358
359
# File 'lib/rod/model.rb', line 357

def self.struct_name_for(name)
  name.underscore.gsub(/\//,"__")
end

Instance Method Details

#==(other) ⇒ Object

Default implementation of equality.



110
111
112
# File 'lib/rod/model.rb', line 110

def ==(other)
  self.class == other.class && self.rod_id == other.rod_id
end

#attributesObject

Returns a hash => ‘attr_value’ which covers fields and has_one relationships values. This is required by ActiveModel::Dirty.



134
135
136
137
138
139
140
141
# File 'lib/rod/model.rb', line 134

def attributes
  result = {}
  self.class.properties.each do |property|
    next if property.association? && property.plural?
    result[property.name.to_s] = self.send(property.name)
  end
  result
end

#dupObject

Returns duplicated object, which shares the state of fields and associations, but is separatly persisted (has its own rod_id, dirty attributes, etc.). WARNING: This behaviour might change slightly in future #157



47
48
49
50
51
52
53
# File 'lib/rod/model.rb', line 47

def dup
  object = super()
  object.instance_variable_set("@rod_id",0)
  object.instance_variable_set("@reference_updaters",@reference_updaters.dup)
  object.instance_variable_set("@changed_attributes",@changed_attributes.dup)
  object
end

#inspectObject

Default implementation of inspect.



120
121
122
123
124
125
# File 'lib/rod/model.rb', line 120

def inspect
  fields = self.class.fields.map{|p| "#{p.name}:#{self.send(p.name)}"}.join(",")
  singular = self.class.singular_associations.map{|p| "#{p.name}:#{self.send(p.name).class}"}.join(",")
  plural = self.class.plural_associations.map{|p| "#{p.name}:#{self.send(p.name).size}"}.join(",")
  "#{self.class}:<#{fields}><#{singular}><#{plural}>"
end

#new?Boolean

Returns true if the object hasn’t been persisted yet.

Returns:

  • (Boolean)


115
116
117
# File 'lib/rod/model.rb', line 115

def new?
  @rod_id == 0
end

#rod_idObject

Return the ROD identifier of the object.



56
57
58
# File 'lib/rod/model.rb', line 56

def rod_id
  @rod_id
end

#store(validate = true) ⇒ Object

Stores the instance in the database. This might be called only if the database is opened for writing (see create). To skip validation pass false.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rod/model.rb', line 67

def store(validate=true)
  if validate
    if valid?
      self.class.store(self)
    else
      raise ValidationException.new([self.to_s,self.errors.full_messages])
    end
  else
    self.class.store(self)
  end
  # The default values doesn't have to be persisted, since they
  # are returned by default by the accessors.
  self.changed.each do |property_name|
    property = self.class.property(property_name.to_sym)
    if property.field?
      # store field value
      update_field(property)
    elsif property.singular?
      # store singular association value
      update_singular_association(property,send(property_name))
    else
      # Plural associations are not tracked.
      raise RodException.new("Invalid changed property #{self.class}##{property}'")
    end
  end
  # store plural associations in the DB
  self.class.plural_associations.each do |property|
    collection = send(property.name)
    offset = collection.save
    update_count_and_offset(property,collection.size,offset)
  end
  # notify reference updaters
  reference_updaters.each do |updater|
    updater.update(self)
  end
  reference_updaters.clear
  # XXX we don't use the 'previously changed' feature, since the simplest
  # implementation requires us to leave references to objects, which
  # forbids them to be garbage collected.
  @changed_attributes.clear unless @changed_attributes.nil?
end

#to_sObject

Default implementation of to_s.



128
129
130
# File 'lib/rod/model.rb', line 128

def to_s
  self.inspect
end

#update_count_and_offset(property, count, offset) ⇒ Object

Updates in the DB the count and offset of elements for property association.



287
288
289
290
# File 'lib/rod/model.rb', line 287

def update_count_and_offset(property,count,offset)
  send("_#{property.name}_count=",@rod_id,count)
  send("_#{property.name}_offset=",@rod_id,offset)
end

#update_field(property) ⇒ Object

Updates in the DB the field property to the actual value.



293
294
295
296
297
298
299
300
301
302
# File 'lib/rod/model.rb', line 293

def update_field(property)
  if property.variable_size?
    value = property.dump(send(property.name))
    length, offset = database.set_string(value)
    send("_#{property.name}_length=",@rod_id,length)
    send("_#{property.name}_offset=",@rod_id,offset)
  else
    send("_#{property.name}=",@rod_id,send(property.name))
  end
end

#update_singular_association(property, object) ⇒ Object

Update the DB information about the object which is referenced via singular association property. If the object is not yet stored, a reference updater is registered to update the DB when it is stored.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/rod/model.rb', line 263

def update_singular_association(property, object)
  if object.nil?
    rod_id = 0
  else
    if object.new?
      # There is a referenced object, but its rod_id is not set.
      object.reference_updaters << ReferenceUpdater.
        for_singular(self,property,self.database)
      return
    else
      rod_id = object.rod_id
    end
    # clear references, allowing for garbage collection
    # WARNING: don't use writer, since we don't want this change to be tracked
    #object.instance_variable_set("@#{name}",nil)
  end
  send("_#{property.name}=", @rod_id, rod_id)
  if property.polymorphic?
    class_id = object.nil? ? 0 : object.class.name_hash
    send("_#{property.name}__class=", @rod_id, class_id)
  end
end