Class: Clevic::CacheTable

Inherits:
Array show all
Includes:
OrderedDataset
Defined in:
lib/clevic/cache_table.rb

Overview

Fetch rows from the db on demand, rather than all up front.

Being able to change the recordset on the fly and still find a previously known entity in the set requires a defined ordering, so if no ordering is specified, the primary key of the entity will be used.

It hasn’t been tested with compound primary keys.

#–

TODO drop rows when they haven’t been accessed for a while

TODO how to handle a quickly-changing underlying table? invalidate cache for each call?

TODO figure out how to handle situations where the character set ordering in the db and in Ruby are different.

Instance Attribute Summary collapse

Attributes included from OrderedDataset

#dataset

Instance Method Summary collapse

Methods included from OrderedDataset

#order_attributes

Methods inherited from Array

#cached_at?, #group, #range, #search, #section, #sparse, #sparse_hash

Constructor Details

#initialize(entity_class, dataset = nil) ⇒ CacheTable

Returns a new instance of CacheTable.



34
35
36
37
38
39
40
41
42
43
# File 'lib/clevic/cache_table.rb', line 34

def initialize( entity_class, dataset = nil )
  @preload_count = 30
  @entity_class = entity_class
  # defined in OrderAttributes
  self.dataset = dataset || entity_class.dataset

  # size the array and fill it with nils. They'll be filled
  # in by the [] operator
  super( sql_count )
end

Instance Attribute Details

#entity_classObject (readonly)

Returns the value of attribute entity_class.



32
33
34
# File 'lib/clevic/cache_table.rb', line 32

def entity_class
  @entity_class
end

#preload_countObject

the number of records loaded in one call to the db



31
32
33
# File 'lib/clevic/cache_table.rb', line 31

def preload_count
  @preload_count
end

Instance Method Details

#[](index) ⇒ Object

return the entity at the given index. Fetch it from the db if it isn’t in this array yet



79
80
81
# File 'lib/clevic/cache_table.rb', line 79

def []( index )
  super( index ) || fetch_entity( index )
end

#compare(key, candidate, direction) ⇒ Object

key is what we’re searching for. candidate is what the current candidate is. direction is 1 for sorted ascending, and -1 for sorted descending TODO retrieve nulls first/last from dataset. In sequel (>3.13.0) this is related to entity_class.filter( :release_date.desc(:nulls=>:first), :name.asc(:nulls=>:last) )



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/clevic/cache_table.rb', line 109

def compare( key, candidate, direction )
  if ( key_nil = key.nil? ) || candidate.nil?
    if key == candidate
      # both nil, ie equal
      0
    else
      # assume nil is sorted greater
      # TODO this should be retrieved from the db
      # ie candidate(nil) <=> key is 1
      # and key <=> candidate(nil) is -1
      key_nil ? -1 : 1
    end
  else
    candidate <=> key
  end * direction
  # reverse the result if we're searching a desc attribute,
  # where direction will be -1
end

#fetch_entity(index) ⇒ Object

Fetch the entity for the given index from the db, and store it in the array. Also, preload preload_count records to avoid subsequent hits on the db



65
66
67
68
69
70
71
72
73
74
75
# File 'lib/clevic/cache_table.rb', line 65

def fetch_entity( index )
  # calculate negative indices for the SQL offset
  offset = index < 0 ? index + sql_count : index

  # fetch self.preload_count records
  records = dataset.limit( preload_count, offset )
  records.each_with_index {|x,i| self[i+index] = x if !cached_at?( i+index )}

  # return the first one
  records.first
end

#index_for_entity(entity) ⇒ Object

find the index for the given entity, using a binary search algorithm (bsearch). The order_by ActiveRecord style options are used to do the binary search. nil is returned if the entity is nil nil is returned if the array is empty



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/clevic/cache_table.rb', line 132

def index_for_entity( entity )
  return nil if size == 0 || entity.nil?

  # only load one record at a time, because mostly we only
  # need one for the binary seach. No point in pulling several out.
  preload_limit( 1 ) do
    # do the binary search based on what we know about the search order
    bsearch do |candidate|
      # find using all sort attributes
      order_attributes.inject(0) do |result,attribute|
        # result from the block should be in [-1,0,1],
        # similar to candidate <=> entity
        key, direction = attribute
        if result == 0
          return nil unless entity.respond_to? key
          return nil unless candidate.respond_to? key

          # compare taking ordering direction into account
          retval = compare( entity.send( key ), candidate.send( key ), direction )

          # exit now because we have a difference
          next( retval ) if retval != 0

          # otherwise try with the next order attribute
          retval
        else
          # recurse out because we have a difference already
          result
        end
      end
    end
  end
end

#preload_limit(limit, &block) ⇒ Object

Execute the block with the specified preload_count, and restore the existing one when done. Return the value of the block



54
55
56
57
58
59
60
# File 'lib/clevic/cache_table.rb', line 54

def preload_limit( limit, &block )
  old_limit = preload_count
  self.preload_count = limit
  retval = yield
  self.preload_count = old_limit
  retval
end

#renew(new_dataset = nil, &block) ⇒ Object

Make a new instance based on the current dataset. Unless new_dataset is specified, pass the dataset to the block, and use the return value from the block as the new dataset.

This is so that filter of datasets can be based on the existing one, but it’s easy to go back to previous data sets if necessary. TODO write tests for both cases.



92
93
94
95
96
97
98
99
100
101
102
# File 'lib/clevic/cache_table.rb', line 92

def renew( new_dataset = nil, &block )
  if new_dataset && block_given?
    raise "Passing a new dataset and a modification block doesn't make sense."
  end

  if block_given?
    self.class.new( entity_class, block.call( dataset ) )
  else
    self.class.new( entity_class, new_dataset || dataset )
  end
end

#sql_countObject

The count of the records according to the db, which may be different to the records in the cache



47
48
49
# File 'lib/clevic/cache_table.rb', line 47

def sql_count
  dataset.count
end