class Moneta::Adapters::ActiveRecord

ActiveRecord as key/value stores @api public

Attributes

table_create_lock[R]
table[R]

Public Class Methods

new(options = {}) click to toggle source

@param [Hash] options @option options [Object] :backend A class object inheriting from ActiveRecord::Base to use as a table @option options [String,Symbol] :table (:moneta) Table name @option options [Hash/String/Symbol] :connection ActiveRecord connection configuration (‘Hash` or `String`), or

symbol giving the name of a Rails connection (e.g. :production)

@option options [Proc, Boolean] :create_table Proc called with a connection if table

needs to be created.  Pass false to skip the create table check all together.

@option options [Symbol] :key_column (:k) The name of the column to use for keys @option options [Symbol] :value_column (:v) The name of the column to use for values

Calls superclass method Moneta::Adapter::new
# File lib/moneta/adapters/activerecord.rb, line 58
def initialize(options = {})
  super
  @table = ::Arel::Table.new(backend.table_name)
end

Public Instance Methods

clear(options = {}) click to toggle source

(see Proxy#clear)

# File lib/moneta/adapters/activerecord.rb, line 146
def clear(options = {})
  with_connection do |conn|
    conn.delete(arel_del)
  end
  self
end
close() click to toggle source

(see Proxy#close)

# File lib/moneta/adapters/activerecord.rb, line 154
def close
  @table = nil
  @connection_pool = nil
end
create(key, value, options = {}) click to toggle source

(see Proxy#create)

# File lib/moneta/adapters/activerecord.rb, line 136
def create(key, value, options = {})
  with_connection do |conn|
    conn_ins(conn, key, value)
    true
  end
rescue ::ActiveRecord::RecordNotUnique
  false
end
delete(key, options = {}) click to toggle source

(see Proxy#delete)

# File lib/moneta/adapters/activerecord.rb, line 98
def delete(key, options = {})
  with_connection do |conn|
    conn.transaction do
      sel = arel_sel_key(key).project(table[config.value_column]).lock
      value = decode(conn, conn.select_value(sel))

      del = arel_del.where(table[config.key_column].eq(key))
      conn.delete(del)

      value
    end
  end
end
each_key() { |k| ... } click to toggle source

(see Proxy#each_key)

# File lib/moneta/adapters/activerecord.rb, line 73
def each_key(&block)
  with_connection do |conn|
    return enum_for(:each_key) { conn.select_value(arel_sel.project(table[config.key_column].count)) } unless block_given?
    conn.select_values(arel_sel.project(table[config.key_column])).each { |k| yield(k) }
  end
  self
end
fetch_values(*keys, **options) { |key| ... } click to toggle source

(see Proxy#fetch_values)

# File lib/moneta/adapters/activerecord.rb, line 199
def fetch_values(*keys, **options)
  return values_at(*keys, **options) unless block_given?
  hash = Hash[slice(*keys, **options)]
  keys.map do |key|
    if hash.key?(key)
      hash[key]
    else
      yield key
    end
  end
end
increment(key, amount = 1, options = {}) click to toggle source

(see Proxy#increment)

# File lib/moneta/adapters/activerecord.rb, line 113
def increment(key, amount = 1, options = {})
  with_connection do |conn|
    conn_ins(conn, key, amount.to_s)
    amount
  rescue ::ActiveRecord::RecordNotUnique
    conn.transaction do
      sel = arel_sel_key(key).project(table[config.value_column]).lock
      value = decode(conn, conn.select_value(sel))
      value = (value ? Integer(value) : 0) + amount
      # Re-raise if the upate affects no rows (i.e. row deleted after attempted insert,
      # before select for update)
      raise unless conn_upd(conn, key, value.to_s) == 1
      value
    end
  end
rescue ::ActiveRecord::RecordNotUnique, ::ActiveRecord::Deadlocked
  # This handles the "no row updated" issue, above, as well as deadlocks
  # which may occur on some adapters
  tries ||= 0
  (tries += 1) <= 3 ? retry : raise
end
key?(key, options = {}) click to toggle source

(see Proxy#key?)

# File lib/moneta/adapters/activerecord.rb, line 64
def key?(key, options = {})
  with_connection do |conn|
    sel = arel_sel_key(key).project(::Arel.sql('1'))
    result = conn.select_all(sel)
    !result.empty?
  end
end
load(key, options = {}) click to toggle source

(see Proxy#load)

# File lib/moneta/adapters/activerecord.rb, line 82
def load(key, options = {})
  with_connection do |conn|
    conn_sel_value(conn, key)
  end
end
merge!(pairs, options = {}) { |key, existing, new_value| ... } click to toggle source

(see Proxy#merge!)

# File lib/moneta/adapters/activerecord.rb, line 212
def merge!(pairs, options = {})
  with_connection do |conn|
    conn.transaction do
      existing = Hash[slice(*pairs.map { |k, _| k }, lock: true, **options)]
      update_pairs, insert_pairs = pairs.partition { |k, _| existing.key?(k) }
      insert_pairs.each { |key, value| conn_ins(conn, key, encode(conn, value)) }

      if block_given?
        update_pairs.map! do |key, new_value|
          [key, yield(key, existing[key], new_value)]
        end
      end

      update_pairs.each { |key, value| conn_upd(conn, key, encode(conn, value)) }
    end
  end

  self
end
slice(*keys, lock: false, **options) click to toggle source

(see Proxy#slice)

# File lib/moneta/adapters/activerecord.rb, line 160
def slice(*keys, lock: false, **options)
  with_connection do |conn|
    conn.create_table(:slice_keys, temporary: true) do |t|
      t.string :key, null: false
    end

    begin
      temp_table = ::Arel::Table.new(:slice_keys)
      keys.each do |key|
        conn.insert ::Arel::InsertManager.new
          .into(temp_table)
          .insert([[temp_table[:key], key]])
      end

      sel = arel_sel
        .join(temp_table)
        .on(table[config.key_column].eq(temp_table[:key]))
        .project(table[config.key_column], table[config.value_column])
      sel = sel.lock if lock
      result = conn.select_all(sel)

      k = config.key_column.to_s
      v = config.value_column.to_s
      result.map do |row|
        [row[k], decode(conn, row[v])]
      end
    ensure
      conn.drop_table(:slice_keys)
    end
  end
end
store(key, value, options = {}) click to toggle source

(see Proxy#store)

# File lib/moneta/adapters/activerecord.rb, line 89
def store(key, value, options = {})
  with_connection do |conn|
    encoded = encode(conn, value)
    conn_ins(conn, key, encoded) unless conn_upd(conn, key, encoded) == 1
  end
  value
end
values_at(*keys, **options) click to toggle source

(see Proxy#values_at)

# File lib/moneta/adapters/activerecord.rb, line 193
def values_at(*keys, **options)
  hash = Hash[slice(*keys, **options)]
  keys.map { |key| hash[key] }
end

Private Instance Methods

arel_del() click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 258
def arel_del
  ::Arel::DeleteManager.new.from(table)
end
arel_sel() click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 262
def arel_sel
  ::Arel::SelectManager.new.from(table)
end
arel_sel_key(key) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 270
def arel_sel_key(key)
  arel_sel.where(table[config.key_column].eq(key))
end
arel_upd() click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 266
def arel_upd
  ::Arel::UpdateManager.new.table(table)
end
conn_ins(conn, key, value) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 274
def conn_ins(conn, key, value)
  ins = ::Arel::InsertManager.new.into(table)
  ins.insert([[table[config.key_column], key], [table[config.value_column], value]])
  conn.insert ins
end
conn_sel_value(conn, key) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 284
def conn_sel_value(conn, key)
  decode(conn, conn.select_value(arel_sel_key(key).project(table[config.value_column])))
end
conn_upd(conn, key, value) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 280
def conn_upd(conn, key, value)
  conn.update arel_upd.where(table[config.key_column].eq(key)).set([[table[config.value_column], value]])
end
decode(conn, value) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 301
def decode(conn, value)
  if value == nil
    nil
  elsif defined?(::ActiveModel::Type::Binary::Data) &&
      value.is_a?(::ActiveModel::Type::Binary::Data)
    value.to_s
  elsif conn.respond_to?(:unescape_bytea)
    conn.unescape_bytea(value)
  else
    value
  end
end
default_create_table(conn, table_name) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 234
def default_create_table(conn, table_name)
  # From 6.1, we can use the `if_not_exists?` check
  if ::ActiveRecord.version < ::Gem::Version.new('6.1.0')
    return if conn.table_exists?(table_name)

    # Prevent multiple connections from attempting to create the table simultaneously.
    self.class.table_create_lock.synchronize do
      conn.create_table(table_name, id: false) do |t|
        # Do not use binary key (Issue #17)
        t.string config.key_column, null: false
        t.binary config.value_column
      end
      conn.add_index(table_name, config.key_column, unique: true)
    end
  else
    conn.create_table(table_name, id: false, if_not_exists: true) do |t|
      # Do not use binary key (Issue #17)
      t.string config.key_column, null: false
      t.binary config.value_column
    end
    conn.add_index(table_name, config.key_column, unique: true, if_not_exists: true)
  end
end
encode(conn, value) click to toggle source
# File lib/moneta/adapters/activerecord.rb, line 288
def encode(conn, value)
  if value == nil
    nil
  elsif conn.respond_to?(:escape_bytea)
    conn.escape_bytea(value)
  elsif defined?(::ActiveRecord::ConnectionAdapters::SQLite3Adapter) &&
      conn.is_a?(::ActiveRecord::ConnectionAdapters::SQLite3Adapter)
    Arel::Nodes::SqlLiteral.new("X'#{value.unpack1('H*')}'")
  else
    value
  end
end