Module: EnumerateBy::Bootstrapped

Defined in:
lib/enumerate_by.rb

Instance Method Summary collapse

Instance Method Details

#bootstrap(*records) ⇒ Object

Synchronizes the given records with existing ones. This ensures that only the correct and most up-to-date records exist in the database. The sync process is as follows:

  • Any existing record that doesn’t match is deleted

  • Existing records with matches are updated based on the given attributes for that record

  • Records that don’t exist are created

To create records that can be referenced elsewhere in the database, an id should always be specified. Otherwise, records may change id each time they are bootstrapped.

Examples

class Color < ActiveRecord::Base
  enumerate_by :name

  bootstrap(
    {:id => 1, :name => 'red'},
    {:id => 2, :name => 'blue'},
    {:id => 3, :name => 'green'}
  )
end

In the above model, the colors table will be synchronized with the 3 records passed into the bootstrap helper. Any existing records that do not match those 3 are deleted. Otherwise, they are either created or updated with the attributes specified.

Defaults

In addition to always synchronizing certain attributes, an additional defaults option can be given to indicate that certain attributes should only be synchronized if they haven’t been modified in the database.

For example,

class Color < ActiveRecord::Base
  enumerate_by :name

  bootstrap(
    {:id => 1, :name => 'red', :defaults => {:html => '#f00'}},
    {:id => 2, :name => 'blue', :defaults => {:html => '#0f0'}},
    {:id => 3, :name => 'green', :defaults => {:html => '#00f'}}
  )
end

In the above model, the name attribute will always be updated on existing records in the database. However, the html attribute will only be synchronized if the attribute is nil in the database. Otherwise, any changes to that column remain there.



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/enumerate_by.rb', line 266

def bootstrap(*records)
  uncached do
    # Remove records that are no longer being used
    records.flatten!
    ids = records.map {|record| record[:id]}.compact
    delete_all(ids.any? ? ['id NOT IN (?)', ids] : nil)
    
    # Find remaining existing records (to be updated)
    existing = all.inject({}) {|existing, record| existing[record.id] = record; existing}
    
    records.map! do |attributes|
      attributes.symbolize_keys!
      defaults = attributes.delete(:defaults)
      
      # Update with new attributes
      record =
        if record = existing[attributes[:id]]
          attributes.merge!(defaults.delete_if {|attribute, value| record.send("#{attribute}?")}) if defaults
          record.attributes = attributes
          record
        else
          attributes.merge!(defaults) if defaults
          new(attributes)
        end
      record.id = attributes[:id]
      
      # Force failed saves to stop execution
      record.save!
      record
    end
    
    records
  end
end

#fast_bootstrap(*records) ⇒ Object

Quickly synchronizes the given records with the existing ones. This skips ActiveRecord altogether, interacting directly with the connection instead. As a result, certain features are not available when being bootstrapped, including:

  • Callbacks

  • Validations

  • Transactions

  • Timestamps

  • Dirty attributes

Also note that records are created directly without creating instances of the model. As a result, all of the attributes for the record must be specified.

This produces a significant performance increase when bootstrapping more than several hundred records.

See EnumerateBy::Bootstrapped#bootstrap for information about usage.



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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/enumerate_by.rb', line 319

def fast_bootstrap(*records)
  # Remove records that are no longer being used
  records.flatten!
  ids = records.map {|record| record[:id]}.compact
  delete_all(ids.any? ? ['id NOT IN (?)', ids] : nil)
  
  # Find remaining existing records (to be updated)
  quoted_table_name = self.quoted_table_name
  existing = connection.select_all("SELECT * FROM #{quoted_table_name}").inject({}) {|existing, record| existing[record['id'].to_i] = record; existing}
  
  records.each do |attributes|
    attributes.stringify_keys!
    if defaults = attributes.delete('defaults')
      defaults.stringify_keys!
    end
    
    id = attributes['id']
    if existing_attributes = existing[id]
      # Record exists: Update attributes
      attributes.delete('id')
      attributes.merge!(defaults.delete_if {|attribute, value| !existing_attributes[attribute].nil?}) if defaults
      update_all(attributes, :id => id)
    else
      # Record doesn't exist: create new one
      attributes.merge!(defaults) if defaults
      column_names = []
      values = []
      
      attributes.each do |column_name, value|
        column_names << connection.quote_column_name(column_name)
        values << connection.quote(value, columns_hash[column_name])
      end
      
      connection.insert(
        "INSERT INTO #{quoted_table_name} (#{column_names * ', '}) VALUES(#{values * ', '})",
        "#{name} Create", primary_key, id, sequence_name
      )
    end
  end
  
  true
end