Last active
March 24, 2017 15:32
-
-
Save xuanyu-h/8338d422984bfcfe7f0c79e28a5ccc99 to your computer and use it in GitHub Desktop.
a simple redis orm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
从零开始构建一个简单的 ORM
一些名词解释
数据的存储过程
表现层 -> 业务逻辑层 -> 持久层 -> 数据库层
持久化
内存中的数据同步保存到数据库或者永久存储设备
持久层
持久层专门负责持久化工作的逻辑层, 而 ORM(Object Relation Mapping) 是持久层的一部分.
如何写一个300行代码的 ORM(基于 Redis)
先看看最终效果
RedisPersistence源码
对象方法反射
这里用了一个
attribute
的 DSL 来反射对象方法.在类中定义
attribute :name
, 实际上对类的实例对象动态定义了name
和name=()
两个方法, 并且把实际需要存储的值用哈希的形式保存在实例对象的@attributes
属性中.而类属性中的
attributes
用于存储已经定义过的attribute
字段.初始化对象
@attributes
实例变量, 用来存储需要保存进数据库的 key-value 映射关系_assign_attribute
方法动态调用之前在类中已经定义的"#{name}="
方法, 把 key-value 存储在当前对象的@attributes
哈希中存储对象
这里用了 Redis 中的 hash 结构来做持久化, 实际封装的 SQL 语句是
hmset
和incrby
,hmset
用于插入整条新数据,incrby
用于计数, 统计当前类的所有对象查找对象
这里封装的 SQL 语句是
exists
和hgetall
.exists
用于判断对象是否实际存储在 Redis 中,hgetall
用于获取存储的 hash 中所有的 key-value, 最终调用initialize
方法生成我们想要的类示例对象更新对象
对某个已经存在的对象属性进行重新赋值, 或者调用
update
方法进行赋值后, 会根据对象中的@_new_record
实例变量来判断是否进行更新操作, 最终用hset
SQL 语句把更新的内容保存在 Redis 中.删除对象
查找出了对象后, 删除就比较简单了, 由于
@key
实例变量是保存在对象中的, 直接调用del
操作删除数据即可, 计数器 counter 也相应的做减操作使用 ORM 的优点