Skip to content

Instantly share code, notes, and snippets.

@xuanyu-h
Last active March 24, 2017 15:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xuanyu-h/8338d422984bfcfe7f0c79e28a5ccc99 to your computer and use it in GitHub Desktop.
Save xuanyu-h/8338d422984bfcfe7f0c79e28a5ccc99 to your computer and use it in GitHub Desktop.
a simple redis orm
# encoding: utf-8
require 'redis'
require 'thread'
begin
require "active_model/model/validations"
rescue LoadError
end
module RedisPersistence
# = Redis \Persistence
#
# Usage:
#
# class User
# include RedisPersistence
# self.redis = Redis.new
#
# attribute :age, ->(i) { i.to_i }
# attribute :name
#
# validates :name, presence: true
# end
#
# u = User.new(key: 1, age: 10, name: 'test')
# u.new_record? # => true
# u.valid? # => true
# u.save # => true
# User.count # => 1
#
# u = User.find(1)
# u.persisted? # => true
# u.age # => 10
#
def self.included(base)
base.extend ClassMethods
base.include ::ActiveModel::Validations
end
class RedisPersistenceError < StandardError; end
class RedisConnectionError < RedisPersistenceError; end
class StoreKeyMissing < RedisPersistenceError; end
class RecordAlreadyExist < RedisPersistenceError; end
class RecordNotSaved < RedisPersistenceError; end
class RecordNotDestroyed < RecordNotSaved; end
class UnknownAttributeError < RedisPersistenceError; end
module ClassMethods
def redis
raise RedisConnectionError if !connected?
@redis
end
def redis=(redis)
@redis = redis
@connected = true
end
def connected?
!!(@redis && @connected)
end
def mutex
@@mutex ||= Mutex.new
end
def synchronize(&block)
mutex.synchronize(&block)
end
def namespace
@namespace ||= self.name
end
def store_key(key)
"#{namespace}:#{key}"
end
def counter
"#{namespace}/counter"
end
def keys(key)
redis.keys(key)
end
def count
redis.get(counter).to_i
end
def find(key)
return nil if !exist?(key)
raw_data = redis.hgetall(store_key(key))
raw_data = raw_data.merge(key: key, _new_record: false)
new(raw_data.to_h)
end
def exist?(key)
redis.exists(store_key(key))
end
def delete(key)
return false if !exist?(key)
synchronize do
if redis.del(store_key(key)) == 1
redis.incrby(counter, -1)
true
else
false
end
end
end
def delete_all
keys("#{namespace}:*").each { |k| redis.del(k) }
redis.del(counter)
true
end
def insert(key, attributes = nil)
flattened_attributes = attributes.to_a.flatten
synchronize do
redis.hmset(store_key(key), flattened_attributes)
redis.incrby(counter, 1)
end
end
def insert_row(key, field, value)
redis.hset(store_key(key), field, value)
end
def attribute(name, block = nil)
attributes << name if !attributes.include?(name)
if block
define_method(name) { block.call(@attributes[name]) }
else
define_method(name) { @attributes[name] }
end
define_method(:"#{name}=") { |value| @attributes[name] = value }
end
# ==== Examples
# # Create a single new object
# User.create(key: '123', first_name: 'Jamie')
#
# # Create an Array of new objects
# User.create([{ key: '123', first_name: 'Jamie' }, { key: '456', first_name: 'Jeremy' }])
def create(attributes = nil)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr) }
else
object = new(attributes)
object.save
object
end
end
def create!(attributes = nil)
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr) }
else
object = new(attributes)
object.save!
object
end
end
protected
def attributes
@attributes ||= []
end
end
attr_reader :key, :attributes
def initialize(attributes = {})
@attributes = {}
@_destroyed = false
@_new_record = attributes.delete(:_new_record).nil? ? true : false
@key = attributes.delete(:key)
raise StoreKeyMissing if !@key
_assign_attributes(attributes)
super()
end
def new_record?
@_new_record
end
def destroyed?
@_destroyed
end
def persisted?
!(@_new_record || @_destroyed)
end
def save(*args)
create_or_update(*args)
end
def save!(*args)
create_or_update(*args) || raise(RecordNotSaved)
end
def delete
if persisted?
result = self.class.delete(key)
return false if !result
end
@_destroyed = true
freeze
end
alias :destroy :delete
def destroy!
destroy || raise(RecordNotDestroyed)
end
def update(attributes)
_assign_attributes(attributes)
save
end
alias update_attributes update
def update!(attributes)
_assign_attributes(attributes)
save!
end
alias update_attributes! update!
def reload
fresh_object = self.class.find(key)
@_new_record = false
_assign_attributes(fresh_object.attributes)
self
end
private
def _assign_attributes(attributes)
attributes.each do |k, v|
_assign_attribute(k, v)
end
end
def _assign_attribute(k, v)
if respond_to?("#{k}=")
public_send("#{k}=", v)
else
raise UnknownAttributeError.new
end
end
def create_or_update(*args)
result = new_record? ? _create_record : _update_record(*args)
result != false
end
def _update_record(attributes = self.attributes)
return false if !key || !valid?
attributes.each do |field, value|
self.class.insert_row(key, field, value)
end
true
end
def _create_record(attributes = self.attributes)
return false if !key || self.class.exist?(key) || !valid?
self.class.insert(key, attributes)
@_new_record = false
true
end
end
@xuanyu-h
Copy link
Author

从零开始构建一个简单的 ORM

一些名词解释

数据的存储过程

表现层 -> 业务逻辑层 -> 持久层 -> 数据库层

持久化

内存中的数据同步保存到数据库或者永久存储设备

持久层

持久层专门负责持久化工作的逻辑层, 而 ORM(Object Relation Mapping) 是持久层的一部分.

如何写一个300行代码的 ORM(基于 Redis)

先看看最终效果

RedisPersistence源码

class User
  include RedisPersistence
  self.redis = Redis.new

  attribute :age, ->(i) { i.to_i }
  attribute :name

  validates :name, presence: true
end

u = User.new(key: 1, age: 10, name: 'test')
u.new_record?       # => true
u.valid?            # => true
u.save              # => true
User.count          # => 1

u = User.find(1)
u.persisted?        # => true
u.age               # => 10
u.update(age: 20)   # => true
u.age               # => 20

对象方法反射

这里用了一个 attribute 的 DSL 来反射对象方法.
在类中定义 attribute :name, 实际上对类的实例对象动态定义了 namename=() 两个方法, 并且把实际需要存储的值用哈希的形式保存在实例对象的 @attributes 属性中.
而类属性中的 attributes 用于存储已经定义过的 attribute 字段.

module ClassMethods
  def attribute(name, block = nil)
   attributes << name if !attributes.include?(name)
  
   if block
     define_method(name) { block.call(@attributes[name]) }
   else
     define_method(name) { @attributes[name] }
   end
  
   define_method(:"#{name}=") { |value|  @attributes[name] = value }
  end
  
  # 类实例变量
  def attributes
    @attributes ||= []
  end
end

初始化对象

  1. 首先定义的是 @attributes 实例变量, 用来存储需要保存进数据库的 key-value 映射关系
  2. 其次定义了需要存储在 redis 中的 key, 这个 key 需要从外部传入, 而不是像其他的数据库一样内部生成自增长的 id
  3. 最终调用 _assign_attribute 方法动态调用之前在类中已经定义的 "#{name}=" 方法, 把 key-value 存储在当前对象的 @attributes 哈希中
  module ClassMethods
    def create(attributes = nil)
      if attributes.is_a?(Array)
        attributes.collect { |attr| create(attr) }
      else
        object = new(attributes)
        object.save
        object
      end
    end
  end

  def initialize(attributes = {})
    @attributes  = {}
    @_destroyed  = false
    @_new_record = attributes.delete(:_new_record).nil? ? true : false
    @key         = attributes.delete(:key)

    raise StoreKeyMissing if !@key

    _assign_attributes(attributes)

    super()
  end
  
  def _assign_attributes(attributes)
    attributes.each do |k, v|
      _assign_attribute(k, v)
    end
  end

  def _assign_attribute(k, v)
    if respond_to?("#{k}=")
      public_send("#{k}=", v)
    else
      raise UnknownAttributeError.new
    end
  end

存储对象

这里用了 Redis 中的 hash 结构来做持久化, 实际封装的 SQL 语句是 hmsetincrby, hmset 用于插入整条新数据, incrby 用于计数, 统计当前类的所有对象

  def save(*args)
    create_or_update(*args)
  end
    
  def _create_record(attributes = self.attributes)
    return false if !key || self.class.exist?(key) || !valid?

    self.class.insert(key, attributes)
    @_new_record = false
    true
  end
  
  module ClassMethods  
    def insert(key, attributes = nil)
      flattened_attributes = attributes.to_a.flatten
      synchronize do
        redis.hmset(store_key(key), flattened_attributes)
        redis.incrby(counter, 1)
      end
    end
  end

查找对象

这里封装的 SQL 语句是 existshgetall.
exists 用于判断对象是否实际存储在 Redis 中, hgetall 用于获取存储的 hash 中所有的 key-value, 最终调用 initialize 方法生成我们想要的类示例对象

  module ClassMethods
    def find(key)
      return nil if !exist?(key)
      raw_data = redis.hgetall(store_key(key))
      raw_data = raw_data.merge(key: key, _new_record: false)

      new(raw_data.to_h)
    end
    
    def exist?(key)
      redis.exists(store_key(key))
    end
  end

更新对象

对某个已经存在的对象属性进行重新赋值, 或者调用 update 方法进行赋值后, 会根据对象中的 @_new_record 实例变量来判断是否进行更新操作, 最终用 hset SQL 语句把更新的内容保存在 Redis 中.

  def update(attributes)
    _assign_attributes(attributes)
    save
  end
  
  def save(*args)
    create_or_update(*args)
  end

  def _update_record(attributes = self.attributes)
    return false if !key || !valid?

    attributes.each do |field, value|
      self.class.insert_row(key, field, value)
    end
    true
  end
  
  module ClassMethods
    def insert_row(key, field, value)
      redis.hset(store_key(key), field, value)
    end
  end

删除对象

查找出了对象后, 删除就比较简单了, 由于 @key 实例变量是保存在对象中的, 直接调用 del 操作删除数据即可, 计数器 counter 也相应的做减操作

  def delete
    if persisted?
      result = self.class.delete(key)
      return false if !result
    end

    @_destroyed = true
    freeze
  end

  module ClassMethods
    def delete(key)
      return false if !exist?(key)
      synchronize do
        if redis.del(store_key(key)) == 1
          redis.incrby(counter, -1)
          true
        else
          false
        end
      end
    end
  end

使用 ORM 的优点

  1. 避免裸写 SQL 语句, 隐藏了数据访问细节, 防止注入
  2. 抽象数据, 把代码与数据库实际的存取逻辑完全分离, 融入现有的 OO 框架

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment