remodel 0.1.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -5,6 +5,10 @@ use [redis](http://github.com/antirez/redis) instead of mysql to store your appl
5
5
  remodel (= redis model) is an ActiveRecord-like mapping layer which offers familiar syntax
6
6
  like `has_many`, `has_one` etc. to build your domain model in ruby.
7
7
 
8
+ entities are serialized to json and stored as fields in a redis hash. using different hashes
9
+ (called 'contexts' in remodel), you can easily separate data belonging to multiple users,
10
+ for example.
11
+
8
12
 
9
13
  ## why redis?
10
14
 
@@ -23,7 +27,7 @@ persistence to disk. for example, on my macbook (2 ghz):
23
27
 
24
28
  ## how to get started
25
29
 
26
- 1. install [redis](http://github.com/antirez/redis) and ezras excellent
30
+ 1. install [redis](http://github.com/antirez/redis) and the
27
31
  [redis-rb](http://github.com/ezmobius/redis-rb) ruby client:
28
32
 
29
33
  $ brew install redis
@@ -49,6 +53,7 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast
49
53
  has_many :chapters, :class => 'Chapter', :reverse => :book
50
54
  property :title, :class => 'String'
51
55
  property :year, :class => 'Integer'
56
+ property :author, :class => 'String', :default => '(anonymous)'
52
57
  end
53
58
 
54
59
  class Chapter < Remodel::Entity
@@ -58,15 +63,20 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast
58
63
 
59
64
  now you can do:
60
65
 
61
- >> require 'example/book'
66
+ >> require './example/book'
62
67
  => true
63
- >> book = Book.create :title => 'Moby Dick', :year => 1851
64
- => #<Book(b:3) title: "Moby Dick", year: 1851>
68
+ >> book = Book.create 'shelf', :title => 'Moby Dick', :year => 1851
69
+ => #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
65
70
  >> chapter = book.chapters.create :title => 'Ishmael'
66
- => #<Chapter(c:4) title: "Ishmael">
71
+ => #<Chapter(shelf, 1) title: "Ishmael">
67
72
  >> chapter.book
68
- => #<Book(b:3) title: "Moby Dick", year: 1851>
73
+ => #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
74
+
75
+ all entities have been created in the redis hash 'shelf' we have used as context:
69
76
 
77
+ >> Remodel.redis.hgetall 'shelf'
78
+ => {"b"=>"1", "b1"=>"{\"title\":\"Moby Dick\",\"year\":1851}", "c"=>"1",
79
+ "c1"=>"{\"title\":\"Ishmael\"}", "c1_book"=>"b1", "b1_chapters"=>"[\"c1\"]"}
70
80
 
71
81
  ## inspired by
72
82
 
@@ -81,16 +91,15 @@ somewhat similar, but instead of serializing to json, stores each attribute unde
81
91
  ## todo
82
92
 
83
93
  * better docs
84
- * `find_by`
85
94
  * make serializer (json, messagepack, marshal ...) configurable
86
- * benchmarks
87
95
 
88
96
 
89
97
  ## status
90
98
 
91
- still pretty alpha &mdash; play around at your own risk :)
99
+ it has some rough edges, but i have successfully been using remodel in production since summer 2010.
100
+
92
101
 
93
102
 
94
103
  ## license
95
104
 
96
- [MIT](http://github.com/tlossen/remodel/raw/master/LICENSE), baby!
105
+ [MIT](http://github.com/tlossen/remodel/raw/master/LICENSE)
data/Rakefile CHANGED
@@ -5,8 +5,8 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "remodel"
8
- gem.summary = "a minimal ORM (object-redis-mapper)"
9
- gem.description = "build your domain model in ruby, persist your objects to redis."
8
+ gem.summary = "remodel variant which uses hashes"
9
+ gem.description = "persist your objects to redis hashes."
10
10
  gem.email = "tim@lossen.de"
11
11
  gem.homepage = "http://github.com/tlossen/remodel"
12
12
  gem.authors = ["Tim Lossen"]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.3.0
data/example/book.rb CHANGED
@@ -4,6 +4,7 @@ class Book < Remodel::Entity
4
4
  has_many :chapters, :class => 'Chapter', :reverse => :book
5
5
  property :title, :class => 'String'
6
6
  property :year, :class => 'Integer'
7
+ property :author, :class => 'String', :default => '(anonymous)'
7
8
  end
8
9
 
9
10
  class Chapter < Remodel::Entity
@@ -2,24 +2,24 @@ module Remodel
2
2
 
3
3
  # The superclass of all persistent remodel entities.
4
4
  class Entity
5
- attr_accessor :key
5
+ attr_accessor :context, :key
6
6
 
7
- def initialize(attributes = {}, key = nil)
7
+ def initialize(context, attributes = {}, key = nil)
8
+ @context = context
8
9
  @attributes = {}
9
10
  @key = key
10
- attributes = self.class.default_values.merge(attributes) if key.nil?
11
11
  attributes.each do |name, value|
12
12
  send("#{name}=", value) if respond_to? "#{name}="
13
13
  end
14
14
  end
15
15
 
16
16
  def id
17
- key && key.split(':').last.to_i
17
+ key && key.match(/\d+/)[0].to_i
18
18
  end
19
19
 
20
20
  def save
21
- @key = self.class.next_key unless @key
22
- Remodel.redis.set(@key, to_json)
21
+ @key = next_key unless @key
22
+ Remodel.redis.hset(@context, @key, to_json)
23
23
  self
24
24
  end
25
25
 
@@ -30,20 +30,25 @@ module Remodel
30
30
 
31
31
  def reload
32
32
  raise EntityNotSaved unless @key
33
- initialize(self.class.parse(self.class.fetch(@key)), @key)
34
- instance_variables.each do |var|
35
- remove_instance_variable(var) if var =~ /^@association_/
33
+ attributes = self.class.parse(self.class.fetch(@context, @key))
34
+ initialize(@context, attributes, @key)
35
+ self.class.associations.each do |name|
36
+ var = "@#{name}".to_sym
37
+ remove_instance_variable(var) if instance_variable_defined? var
36
38
  end
37
39
  self
38
40
  end
39
41
 
40
42
  def delete
41
43
  raise EntityNotSaved unless @key
42
- Remodel.redis.del(@key)
44
+ Remodel.redis.hdel(@context, @key)
45
+ self.class.associations.each do |name|
46
+ Remodel.redis.hdel(@context, "#{@key}_#{name}")
47
+ end
43
48
  end
44
49
 
45
50
  def as_json
46
- { :id => id }.merge(@attributes)
51
+ { :id => id }.merge(attributes)
47
52
  end
48
53
 
49
54
  def to_json
@@ -51,29 +56,21 @@ module Remodel
51
56
  end
52
57
 
53
58
  def inspect
54
- properties = @attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
55
- "\#<#{self.class.name}(#{id}) #{properties}>"
56
- end
57
-
58
- def self.create(attributes = {})
59
- new(attributes).save
59
+ properties = attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
60
+ "\#<#{self.class.name}(#{context}, #{id}) #{properties}>"
60
61
  end
61
62
 
62
- def self.find(key)
63
- key = "#{key_prefix}:#{key}" if key.kind_of? Integer
64
- restore(key, fetch(key))
63
+ def self.create(context, attributes = {})
64
+ new(context, attributes).save
65
65
  end
66
66
 
67
- def self.all
68
- keys = Remodel.redis.keys("#{key_prefix}:*").select { |k| k =~ /:[0-9]+$/ }
69
- values = keys.empty? ? [] : Remodel.redis.mget(keys)
70
- keys.zip(values).map do |key, json|
71
- restore(key, json) if json
72
- end.compact
67
+ def self.find(context, key)
68
+ key = "#{key_prefix}#{key}" if key.kind_of? Integer
69
+ restore(context, key, fetch(context, key))
73
70
  end
74
71
 
75
- def self.restore(key, json)
76
- new(parse(json), key)
72
+ def self.restore(context, key, json)
73
+ new(context, parse(json), key)
77
74
  end
78
75
 
79
76
  protected # --- DSL for subclasses ---
@@ -86,34 +83,37 @@ module Remodel
86
83
  def self.property(name, options = {})
87
84
  name = name.to_sym
88
85
  mapper[name] = Remodel.mapper_for(options[:class])
89
- default_values[name] = options[:default] if options.has_key?(:default)
90
- define_method(name) { @attributes[name] }
86
+ default_value = options[:default]
87
+ define_method(name) { @attributes[name].nil? ? self.class.copy_of(default_value) : @attributes[name] }
91
88
  define_method("#{name}=") { |value| @attributes[name] = value }
92
89
  end
93
90
 
94
91
  def self.has_many(name, options)
95
- var = "@association_#{name}".to_sym
92
+ associations.push(name)
93
+ var = "@#{name}".to_sym
96
94
 
97
95
  define_method(name) do
98
96
  if instance_variable_defined? var
99
97
  instance_variable_get(var)
100
98
  else
101
99
  clazz = Class[options[:class]]
102
- instance_variable_set(var, HasMany.new(self, clazz, "#{key}:#{name}", options[:reverse]))
100
+ instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{name}", options[:reverse]))
103
101
  end
104
102
  end
105
103
  end
106
104
 
107
105
  def self.has_one(name, options)
108
- var = "@association_#{name}".to_sym
106
+ associations.push(name)
107
+ var = "@#{name}".to_sym
109
108
 
110
109
  define_method(name) do
111
110
  if instance_variable_defined? var
112
111
  instance_variable_get(var)
113
112
  else
114
113
  clazz = Class[options[:class]]
115
- value_key = Remodel.redis.get("#{key}:#{name}")
116
- instance_variable_set(var, clazz.find(value_key)) if value_key
114
+ value_key = Remodel.redis.hget(self.context, "#{key}_#{name}")
115
+ value = value_key && clazz.find(self.context, value_key) rescue nil
116
+ instance_variable_set(var, value)
117
117
  end
118
118
  end
119
119
 
@@ -125,10 +125,10 @@ module Remodel
125
125
  define_method("_#{name}=") do |value|
126
126
  if value
127
127
  instance_variable_set(var, value)
128
- Remodel.redis.set("#{key}:#{name}", value.key)
128
+ Remodel.redis.hset(self.context, "#{key}_#{name}", value.key)
129
129
  else
130
130
  remove_instance_variable(var) if instance_variable_defined? var
131
- Remodel.redis.del("#{key}:#{name}")
131
+ Remodel.redis.hdel(self.context, "#{key}_#{name}")
132
132
  end
133
133
  end; private "_#{name}="
134
134
 
@@ -156,14 +156,22 @@ module Remodel
156
156
 
157
157
  private # --- Helper methods ---
158
158
 
159
- def self.fetch(key)
160
- Remodel.redis.get(key) || raise(EntityNotFound, "no #{name} with key #{key}")
159
+ def attributes
160
+ result = {}
161
+ self.class.mapper.keys.each do |name|
162
+ result[name] = send(name)
163
+ end
164
+ result
165
+ end
166
+
167
+ def self.fetch(context, key)
168
+ Remodel.redis.hget(context, key) || raise(EntityNotFound, "no #{name} with key #{key} in context #{context}")
161
169
  end
162
170
 
163
171
  # Each entity has its own sequence to generate unique ids.
164
- def self.next_key
165
- id = Remodel.redis.incr("#{key_prefix}:seq")
166
- "#{key_prefix}:#{id}"
172
+ def next_key
173
+ id = Remodel.redis.hincrby(@context, "#{self.class.key_prefix}", 1)
174
+ "#{self.class.key_prefix}#{id}"
167
175
  end
168
176
 
169
177
  # Default key prefix is the first letter of the class name, in lowercase.
@@ -187,7 +195,7 @@ module Remodel
187
195
  result = {}
188
196
  attributes.each do |name, value|
189
197
  name = name.to_sym
190
- result[name] = mapper[name].unpack(value)
198
+ result[name] = mapper[name].unpack(value) if mapper[name]
191
199
  end
192
200
  result
193
201
  end
@@ -197,8 +205,12 @@ module Remodel
197
205
  @mapper ||= {}
198
206
  end
199
207
 
200
- def self.default_values
201
- @default_values ||= {}
208
+ def self.associations
209
+ @associations ||= []
210
+ end
211
+
212
+ def self.copy_of(value)
213
+ value.is_a?(Array) || value.is_a?(Hash) ? value.dup : value
202
214
  end
203
215
 
204
216
  end
@@ -3,12 +3,12 @@ module Remodel
3
3
  # Represents the many-end of a many-to-one or many-to-many association.
4
4
  class HasMany < Array
5
5
  def initialize(this, clazz, key, reverse = nil)
6
- super _fetch(clazz, key)
6
+ super _fetch(clazz, this.context, key)
7
7
  @this, @clazz, @key, @reverse = this, clazz, key, reverse
8
8
  end
9
9
 
10
10
  def create(attributes = {})
11
- add(@clazz.create(attributes))
11
+ add(@clazz.create(@this.context, attributes))
12
12
  end
13
13
 
14
14
  def find(id)
@@ -29,13 +29,13 @@ module Remodel
29
29
 
30
30
  def _add(entity)
31
31
  self << entity
32
- Remodel.redis.rpush(@key, entity.key)
32
+ _store
33
33
  entity
34
34
  end
35
35
 
36
36
  def _remove(entity)
37
37
  delete_if { |x| x.key == entity.key }
38
- Remodel.redis.lrem(@key, 0, entity.key)
38
+ _store
39
39
  entity
40
40
  end
41
41
 
@@ -55,11 +55,15 @@ module Remodel
55
55
  end
56
56
  end
57
57
 
58
- def _fetch(clazz, key)
59
- keys = Remodel.redis.lrange(key, 0, -1)
60
- values = keys.empty? ? [] : Remodel.redis.mget(keys)
58
+ def _store
59
+ Remodel.redis.hset(@this.context, @key, JSON.generate(self.map(&:key)))
60
+ end
61
+
62
+ def _fetch(clazz, context, key)
63
+ keys = JSON.parse(Remodel.redis.hget(context, key) || '[]').uniq
64
+ values = keys.empty? ? [] : Remodel.redis.hmget(context, *keys)
61
65
  keys.zip(values).map do |key, json|
62
- clazz.restore(key, json) if json
66
+ clazz.restore(context, key, json) if json
63
67
  end.compact
64
68
  end
65
69
  end
data/lib/remodel.rb CHANGED
@@ -34,6 +34,7 @@ module Remodel
34
34
  class EntityNotSaved < Error; end
35
35
  class InvalidKeyPrefix < Error; end
36
36
  class InvalidType < Error; end
37
+ class MissingContext < Error; end
37
38
 
38
39
  # By default, the redis server is expected to listen at `localhost:6379`.
39
40
  # Otherwise you will have to set `Remodel.redis` to a suitably initialized
@@ -45,7 +46,7 @@ module Remodel
45
46
  def self.redis=(redis)
46
47
  @redis = redis
47
48
  end
48
-
49
+
49
50
  # Returns the mapper defined for a given class, or the identity mapper.
50
51
  def self.mapper_for(clazz)
51
52
  mapper_by_class[Class[clazz]]
data/test/helper.rb CHANGED
@@ -9,5 +9,5 @@ class Test::Unit::TestCase
9
9
  def redis
10
10
  Remodel.redis
11
11
  end
12
-
12
+
13
13
  end