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.



162
163
164
165
166
167
168
# File 'lib/rod/model.rb', line 162

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.



140
141
142
143
144
145
146
# File 'lib/rod/model.rb', line 140

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.



149
150
151
152
153
154
155
156
157
158
# File 'lib/rod/model.rb', line 149

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.



366
367
368
369
370
371
372
# File 'lib/rod/model.rb', line 366

def self.fields
  if self == Rod::Model
    @fields ||= [Property::Field.new(self,:rod_id,:ulong)]
  else
    @fields ||= superclass.fields.map{|p| p.copy(self)}
  end
end

.find_by_rod_id(rod_id) ⇒ Object

Finder for rod_id.



358
359
360
361
362
363
# File 'lib/rod/model.rb', line 358

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.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/rod/model.rb', line 413

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.



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

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

.metadataObject

Metadata for the model class.



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/rod/model.rb', line 393

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.



437
438
439
440
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
# File 'lib/rod/model.rb', line 437

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.



384
385
386
387
388
389
390
# File 'lib/rod/model.rb', line 384

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.



785
786
787
# File 'lib/rod/model.rb', line 785

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.



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

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

.singular_associationsObject

Returns singular associations of this class.



375
376
377
378
379
380
381
# File 'lib/rod/model.rb', line 375

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.



306
307
308
309
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
# File 'lib/rod/model.rb', line 306

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.



342
343
344
345
346
347
348
349
350
# File 'lib/rod/model.rb', line 342

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.



353
354
355
# File 'lib/rod/model.rb', line 353

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

Instance Method Details

#==(other) ⇒ Object

Default implementation of equality.



105
106
107
# File 'lib/rod/model.rb', line 105

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.



129
130
131
132
133
134
135
136
# File 'lib/rod/model.rb', line 129

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.



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

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)


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

def new?
  @rod_id == 0
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.



62
63
64
65
66
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
# File 'lib/rod/model.rb', line 62

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.



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

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.



283
284
285
286
# File 'lib/rod/model.rb', line 283

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.



289
290
291
292
293
294
295
296
297
298
# File 'lib/rod/model.rb', line 289

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.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/rod/model.rb', line 259

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