Rubernate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,30 @@
1
+ == Rubernate
2
+
3
+ Rubernate is an object-oriented storage for Ruby objects based on relational database model.
4
+
5
+ Rubernate provides an ability to create persistent object hierarchies with minimal restrictions
6
+ on their structure. The main difference of Rubernate from traditional ORM is that it uses common
7
+ database tables to store all classes of persistent objects. All object's-related data are stored in
8
+ fixed set of tables.
9
+
10
+ This approach has following advantages:
11
+
12
+ * Simplicity - it simplifies greatly persistent classes creation and their modifications, too, because
13
+ it prevents database structure changes.
14
+
15
+ * Reusability - as persistence doesn't depend on database structure its possible to reuse objects in different projects.
16
+
17
+ * Object oriented storage - it is possible to put conditions on objects classes during a search.
18
+
19
+ Rubernate has following features:
20
+
21
+ * Classes can have persistent parameters of following types: Integer, String, Date, Time, Reference to Persistent Object, Array of References, Hash of References with key of type Integer or String or Date or Time.
22
+
23
+ * Built-in query language and support for native queries
24
+
25
+ * Support for Oracle and MySQL.
26
+
27
+ * Can be used independently or with other ORM like ActiveRecord
28
+
29
+ * Extendable architecture - can be easily extended by user functionality.
30
+
data/db/mysql.sql ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Creates Rubernate tables for MySQL database.
3
+ * Copyright (C) 2006 Andrey Ryabov <andrey_ryabov@bk.ru>
4
+ */
5
+
6
+ # Creates database named rubernate_db change name and uncomment if necessary
7
+ # CREATE DATABASE RUBERNATE_DB;
8
+ # USE RUBERNATE_DB;
9
+
10
+
11
+ DROP TABLE IF EXISTS R_PARAMS;
12
+ DROP TABLE IF EXISTS R_OBJECTS;
13
+
14
+ CREATE TABLE R_OBJECTS (
15
+ OBJECT_PK INTEGER(20) PRIMARY KEY AUTO_INCREMENT,
16
+ OBJECT_CLASS VARCHAR(100) NOT NULL) ENGINE=InnoDB;
17
+
18
+ CREATE TABLE R_PARAMS (
19
+ OBJECT_PK INTEGER(20) NOT NULL,
20
+ NAME VARCHAR(100) NOT NULL,
21
+ FLAGS INTEGER(5) NOT NULL,
22
+ INT_VALUE INTEGER(20),
23
+ STR_VALUE VARCHAR(255),
24
+ DAT_VALUE DATETIME,
25
+ REF_VALUE INTEGER(20),
26
+ CONSTRAINT R_PARAM_FK FOREIGN KEY (OBJECT_PK) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE,
27
+ CONSTRAINT R_REF_FK FOREIGN KEY (REF_VALUE) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE) ENGINE=InnoDB;
28
+
29
+ CREATE INDEX R_O_PK_CLASS ON R_OBJECTS (OBJECT_PK ASC, OBJECT_CLASS);
30
+ CREATE INDEX R_P_PK_NAME ON R_PARAMS (OBJECT_PK ASC, NAME);
31
+
32
+
33
+ /**
34
+ * End
35
+ */
data/db/oracle.sql ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Creates Rubernate tables for Oracle database.
3
+ * Copyright (C) 2006 Andrey Ryabov <andrey_ryabov@bk.ru>
4
+ */
5
+
6
+ CREATE TABLE R_OBJECTS (
7
+ OBJECT_PK NUMBER(20) PRIMARY KEY,
8
+ OBJECT_CLASS VARCHAR2(100) NOT NULL)
9
+ /
10
+ CREATE TABLE R_PARAMS (
11
+ OBJECT_PK NUMBER(20) NOT NULL,
12
+ NAME VARCHAR2(100) NOT NULL,
13
+ FLAGS NUMBER(5) NOT NULL,
14
+ INT_VALUE NUMBER(20),
15
+ STR_VALUE VARCHAR2(1000),
16
+ DAT_VALUE DATE,
17
+ REF_VALUE NUMBER(20),
18
+ CONSTRAINT R_PARAM_FK FOREIGN KEY (OBJECT_PK) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE,
19
+ CONSTRAINT R_REF_FK FOREIGN KEY (REF_VALUE) REFERENCES R_OBJECTS(OBJECT_PK) ON DELETE CASCADE)
20
+ /
21
+ CREATE INDEX R_O_PK_CLASS ON R_OBJECTS (OBJECT_PK ASC, OBJECT_CLASS)
22
+ /
23
+ CREATE INDEX R_P_PK_NAME ON R_PARAMS (OBJECT_PK ASC, NAME)
24
+ /
25
+ CREATE SEQUENCE R_PK_SEQUENCE START WITH 1001 INCREMENT BY 1
26
+ /
27
+ /**
28
+ * End
29
+ */
@@ -0,0 +1,70 @@
1
+ module Rubernate
2
+
3
+ # This module is holder for other callback modules.
4
+ # Methods in these modules are intended to be overridden by user
5
+ # in other to extend basic functionality.
6
+ module Callbacks
7
+ # This module contains callback methods and
8
+ # included in module Rubernate::Entity
9
+ module Entity
10
+ # Invoked if object has just been created in database.
11
+ def on_create
12
+ end
13
+
14
+ # Invoked when object is about to be removed from database.
15
+ def on_remove
16
+ end
17
+
18
+ # Invoked when object is about to be stored in database.
19
+ def on_save
20
+ end
21
+
22
+ # Invoked if object has just been loaded from database.
23
+ def on_load
24
+ end
25
+
26
+ # Invoked if property of object has just been reassigned by +=+ operator.
27
+ def on_change prop_name, old_value, new_value
28
+ end
29
+
30
+ # Invoked if Hash or Array property has been modified.
31
+ def on_modify prop_name, old_value, new_value
32
+ end
33
+
34
+ # Invoked if object referred by this one has been deleted
35
+ # TODO: implement invokations
36
+ def on_lose_ref refered_by
37
+ end
38
+ end
39
+
40
+ # This module contains callback method and
41
+ # included in class Rubernate::Runtime.
42
+ module Runtime
43
+ # Invoked on session creation.
44
+ def on_begin
45
+ end
46
+
47
+ # Invoked if session is about to be rolled back.
48
+ def on_rollback
49
+ end
50
+
51
+ # Invoked if session is about to be commited.
52
+ def before_commit
53
+ end
54
+
55
+ # Invoked if session has just been commited
56
+ def after_commit
57
+ end
58
+
59
+ # Invoked when Rubernate needs to flush modified objects
60
+ # It can happens when find_by_query is called or if session is going to be commited
61
+ def before_flush modified
62
+ end
63
+
64
+ # Invoked if after flushing modified objects
65
+ # This method is invoked only if any objects has been actually flushed
66
+ def after_flush
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,85 @@
1
+ module Rubernate
2
+
3
+ # This module contains methods that will be included into persistent objects
4
+ module Entity
5
+ include Rubernate
6
+ include Callbacks::Entity
7
+
8
+ # This property holds objects primary key.
9
+ # The primary key is not +nil+ for objects attached to session.
10
+ attr :primary_key, true
11
+
12
+ # Attaches object to session returns self
13
+ def attach
14
+ Rubernate.runtime.attach self
15
+ self
16
+ end
17
+
18
+ # Deletes object.
19
+ def remove!
20
+ Rubernate.runtime.remove self
21
+ @removed = true
22
+ end
23
+
24
+ # Checks if object has been removed in current session
25
+ def removed?
26
+ @removed
27
+ end
28
+
29
+ # Retruns objects +peer+ if ther is one or +nil+ in other case
30
+ def peer
31
+ @peer
32
+ end
33
+
34
+ # Sets peer if there is no one and invokes on_load callback else skip.
35
+ # If value is _nil_ peer will be lazy initialized in ensure_loaded.
36
+ # This method should be invoked only once with not nil value.
37
+ def peer= value
38
+ return if @peer
39
+ if value
40
+ disable_ensure
41
+ else
42
+ def self.ensure_loaded
43
+ Rubernate.runtime.find_by_pk @primary_key, true
44
+ end
45
+ end
46
+ @peer = value
47
+ end
48
+
49
+ def dirty?
50
+ peer.dirty? &on_modify_callback
51
+ end
52
+
53
+ # Defines metod == in for rubernated classes
54
+ def self.included klass
55
+ klass.module_eval %{
56
+ def == other
57
+ return self.primary_key == other.primary_key if other.is_a? Entity
58
+ super
59
+ end
60
+ }
61
+ end
62
+
63
+ private
64
+ def ensure_loaded
65
+ self.peer = Peer.new
66
+ end
67
+
68
+ def disable_ensure
69
+ def self.ensure_loaded
70
+ end
71
+ end
72
+
73
+ def on_modify_callback
74
+ @__on_modify_callback ||= proc {|p_name, old_value, new_value|
75
+ on_modify p_name, old_value, new_value # callback
76
+ }
77
+ end
78
+
79
+ def on_change_callback
80
+ @__on_change_callback ||= proc {|p_name, old_value, new_value|
81
+ on_change p_name, old_value, new_value # callback
82
+ }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,286 @@
1
+ require 'dbi'
2
+ require 'time'
3
+
4
+ module Rubernate
5
+ module DBI
6
+ # Param flags constants
7
+ PARAM_FLAG_INT = (1 << 0).to_i
8
+ PARAM_FLAG_STRING = (1 << 1).to_i
9
+ PARAM_FLAG_TIME = (1 << 2).to_i
10
+ PARAM_FLAG_DATE = (1 << 3).to_i
11
+ PARAM_FLAG_REF = (1 << 4).to_i
12
+ PARAM_FLAG_ARRAY = (1 << 5).to_i
13
+ PARAM_FLAG_HASH = (1 << 6).to_i
14
+
15
+ # Array and Hashs combinations
16
+ ARRAY_INT_REF = (PARAM_FLAG_ARRAY | PARAM_FLAG_INT).to_i
17
+ HASH_INT_REF = (PARAM_FLAG_HASH | PARAM_FLAG_INT).to_i
18
+ HASH_STRING_REF = (PARAM_FLAG_HASH | PARAM_FLAG_STRING).to_i
19
+ HASH_TIME_REF = (PARAM_FLAG_HASH | PARAM_FLAG_TIME).to_i
20
+ HASH_DATE_REF = (PARAM_FLAG_HASH | PARAM_FLAG_DATE).to_i
21
+
22
+ # Represents factory for +Runtime+ implementors.
23
+ class RuntimeFactory
24
+ # Accepts +Runtime+ impl. class, database url, user name, and user password
25
+ def initialize klass, db_url, db_user, db_password
26
+ @klass, @db_url, @db_user, @db_password = klass, db_url, db_user, db_password
27
+ # @params = {'AutoCommit' => false} #TODO: refine
28
+ end
29
+
30
+ # Creates initialized PeerHost
31
+ def create
32
+ @klass.new connect
33
+ end
34
+
35
+ # Creates connection and pass it in block if present
36
+ # else return it if there is no block given.
37
+ def connect
38
+ return ::DBI.connect(@db_url, @db_user, @db_password, @params) unless block_given?
39
+ ::DBI.connect(@db_url, @db_user, @db_password, @params) { |dbh| yield dbh }
40
+ end
41
+
42
+ def to_s
43
+ "runtime impl: #{@klass}, db_url: #{@db_url}, db_user: #{@db_user}, db_password: #{@db_password}"
44
+ end
45
+ end
46
+
47
+ # Represent base class for all Runtime implementation.
48
+ # Contains generic sql implementations of all required methods except +create+.
49
+ # The +create+ method should be implemented in subclasses.
50
+ class Runtime < Rubernate::Runtime
51
+ include DBI
52
+
53
+ # Parameters are deleted automatically with corresponding r_object records
54
+ #DELETE_PARAMS = <<-SQL
55
+ # DELETE FROM R_PARAMS WHERE OBJECT_PK = ?
56
+ # SQL
57
+ DELETE_PARAMS_FOR = <<-SQL
58
+ DELETE FROM R_PARAMS WHERE OBJECT_PK IN
59
+ SQL
60
+ DELETE_OBJECT = <<-SQL
61
+ DELETE FROM R_OBJECTS WHERE OBJECT_PK = ?
62
+ SQL
63
+ SAVE_PARAMS = <<-SQL
64
+ INSERT INTO R_PARAMS VALUES (?, ?, ?, ?, ?, ?, ?)
65
+ SQL
66
+ SELECT_PARAMS = <<-SQL
67
+ SELECT P.*, O.OBJECT_CLASS, R.OBJECT_CLASS
68
+ FROM R_PARAMS P JOIN R_OBJECTS O ON (O.OBJECT_PK = P.OBJECT_PK)
69
+ LEFT OUTER JOIN R_OBJECTS R ON (P.REF_VALUE = R.OBJECT_PK)
70
+ WHERE O.OBJECT_PK IN
71
+ SQL
72
+ SELECT_ONE_OBJECT = <<-SQL
73
+ SELECT O.OBJECT_PK, P.NAME, P.FLAGS, P.INT_VALUE, P.STR_VALUE,
74
+ P.DAT_VALUE, P.REF_VALUE, O.OBJECT_CLASS, R.OBJECT_CLASS
75
+ FROM R_OBJECTS O
76
+ LEFT OUTER JOIN R_PARAMS P ON (O.OBJECT_PK = P.OBJECT_PK)
77
+ LEFT OUTER JOIN R_OBJECTS R ON (P.REF_VALUE = R.OBJECT_PK)
78
+ WHERE O.OBJECT_PK = ?
79
+ SQL
80
+
81
+ # R_OBJECTS table's column's indexes
82
+ CI_O_PK = 0
83
+ CI_O_CLASS = 1
84
+
85
+ # R_PARAMS table's column's indexes
86
+ CI_P_PK = 0
87
+ CI_P_NAME = 1
88
+ CI_P_FLAGS = 2
89
+ CI_P_INT = 3
90
+ CI_P_STR = 4
91
+ CI_P_TIME = 5
92
+ CI_P_REF = 6
93
+
94
+ attr :dbh, false
95
+
96
+ def initialize dbh
97
+ super()
98
+ @dbh = dbh
99
+ end
100
+
101
+ # Updates object state in database
102
+ def save objects
103
+ objects = [objects] unless objects.kind_of? Array
104
+ return if objects.empty?
105
+ # clear r_params for objects
106
+ @dbh.do DELETE_PARAMS_FOR + object_ids_p(objects)
107
+ # store r_params for objects
108
+ @dbh.prepare SAVE_PARAMS do |sth|
109
+ for object in objects
110
+ object.peer.each {|name, param|
111
+ save_param sth, object.primary_key, name.to_s, param
112
+ }
113
+ end
114
+ end
115
+ # clear dirty flag
116
+ for object in objects
117
+ object.peer.dirty = false
118
+ end
119
+ end
120
+
121
+ # Loads object by primary_key
122
+ def load_by_pk pk
123
+ klass, peer = nil
124
+ dbh.execute(SELECT_ONE_OBJECT, pk) { |sth|
125
+ sth.fetch { |row|
126
+ klass, peer = row[7], Rubernate::Peer.new unless klass
127
+ fetch_param peer, row
128
+ }
129
+ }
130
+ return nil unless klass
131
+ obj = instantiate pk, class_by_name(klass), peer
132
+ post_load obj
133
+ obj
134
+ end
135
+
136
+ # Loads objects by query with specified params.
137
+ def load_by_query query, params=[]
138
+ result = []
139
+ buffer = {} # buffer for not loaded objects (whose peer is nil)
140
+ # selects objects and stores not loaded to buffer
141
+ dbh.execute(query, *params) { |sth|
142
+ sth.fetch { |row|
143
+ obj = instantiate row[0].to_i, class_by_name(row[1])
144
+ buffer[obj.primary_key], obj.peer = obj, Rubernate::Peer.new unless obj.peer
145
+ result << obj
146
+ }
147
+ }
148
+ return result if buffer.empty?
149
+ # load peers for objects in buffer.
150
+ dbh.execute(SELECT_PARAMS + object_ids_p(buffer.values)) {|sth|
151
+ sth.fetch {|row|
152
+ fetch_param buffer[row[0].to_i].peer, row
153
+ }
154
+ }
155
+ # clears dirty flags
156
+ buffer.values.each {|object| post_load object}
157
+ result
158
+ end
159
+
160
+ # Deletes object from database
161
+ def delete object
162
+ # Rows from r_params should be deleted automaticaly
163
+ # by foregin key contraint
164
+ #dbh.do DELETE_PARAMS, object.primary_key
165
+ dbh.do DELETE_OBJECT, object.primary_key
166
+ object.primary_key = nil
167
+ end
168
+
169
+ def close
170
+ @dbh.commit
171
+ @dbh.disconnect
172
+ end
173
+
174
+ def failed
175
+ @dbh.rollback rescue Log.error 'Error during rollback in session failed state'
176
+ @dbh.disconnect rescue Log.error 'Error during disconnect in session failed state'
177
+ end
178
+ private
179
+ def save_param sth, pk, name, param
180
+ case param
181
+ when Entity: save_ref sth, pk, name, param
182
+ when Hash: save_hash sth, pk, name, param
183
+ when Array: save_array sth, pk, name, param
184
+ when Integer: save_int sth, pk, name, param
185
+ when Time: save_time sth, pk, name, param
186
+ when Date: save_date sth, pk, name, param
187
+ else save_str sth, pk, name, param
188
+ end
189
+ end
190
+
191
+ def save_ref sth, pk, name, ref
192
+ sth.execute pk, name, PARAM_FLAG_REF, nil, nil, nil, ref.primary_key unless ref.removed?
193
+ end
194
+
195
+ def save_time sth, pk, name, time
196
+ sth.execute pk, name, PARAM_FLAG_TIME, nil, nil, ::DBI::Timestamp.new(time), nil
197
+ end
198
+
199
+ def save_date sth, pk, name, date
200
+ sth.execute pk, name, PARAM_FLAG_DATE, nil, nil, ::DBI::Timestamp.new(date), nil
201
+ end
202
+
203
+ def save_hash sth, pk, name, hash
204
+ hash.delete_if {|key, ref| ref.removed?}
205
+ if hash.empty?
206
+ sth.execute pk, name, PARAM_FLAG_HASH, 0, nil, nil, nil
207
+ else
208
+ hash.each {|key, ref| save_key_value sth, pk, name, key, ref.primary_key}
209
+ end
210
+ end
211
+
212
+ def save_key_value sth, pk, name, key, ref
213
+ if key.is_a? String
214
+ sth.execute pk, name, HASH_STRING_REF, nil, key, nil, ref
215
+ elsif key.is_a? Integer
216
+ sth.execute pk, name, HASH_INT_REF, key, nil, nil, ref
217
+ elsif key.is_a? ::Time # TODO: chek time and date scopes?
218
+ sth.execute pk, name, HASH_TIME_REF, nil, nil, ::DBI::Timestamp.new(key), ref
219
+ elsif key.is_a? ::Date
220
+ sth.execute pk, name, HASH_DATE_REF, nil, nil, ::DBI::Timestamp.new(key), ref
221
+ elsif key.nil?
222
+ sth.execute pk, name, PARAM_FLAG_HASH, nil, nil, nil, ref
223
+ else
224
+ raise "invalid hash key value #{key}"
225
+ end
226
+ end
227
+
228
+ def save_array sth, pk, name, array
229
+ array.delete_if {|ref| !ref or ref.removed?} # Reject removed and nil items
230
+ if array.empty? # Empty arrays should also be stored
231
+ sth.execute pk, name, ARRAY_INT_REF, 0, nil, nil, nil
232
+ else
233
+ array.each_index { |idx| sth.execute pk, name,
234
+ ARRAY_INT_REF, idx, nil, nil, array[idx].primary_key}
235
+ end
236
+ end
237
+
238
+ def save_int sth, pk, name, param
239
+ sth.execute pk, name, PARAM_FLAG_INT, param, nil, nil, nil
240
+ end
241
+
242
+ def save_str sth, pk, name, param
243
+ sth.execute pk, name, PARAM_FLAG_STRING, nil, param.to_s, nil, nil
244
+ end
245
+
246
+ def fetch_param peer, row
247
+ return unless row[1] # there are no params for this object
248
+ name, flags, ref = row[1].to_sym, row[2].to_i
249
+ if flags & PARAM_FLAG_ARRAY != 0
250
+ peer[name] = [] unless peer[name]
251
+ peer[name][fetch_value(row, flags)] = ref if ref = fetch_ref_value(row)
252
+ elsif flags & PARAM_FLAG_HASH != 0
253
+ peer[name] = {} unless peer[name]
254
+ peer[name][fetch_value(row, flags)] = ref if ref = fetch_ref_value(row)
255
+ else
256
+ peer[name] = fetch_value row, flags
257
+ end
258
+ end
259
+
260
+ def fetch_value row, flags
261
+ return row[CI_P_INT].to_i if flags & PARAM_FLAG_INT != 0
262
+ return row[CI_P_STR].to_s if flags & PARAM_FLAG_STRING != 0
263
+ return instantiate(row[CI_P_REF].to_i, class_by_name(row[CI_P_REF + 2])) if flags & PARAM_FLAG_REF != 0
264
+ return row[CI_P_TIME].to_time if flags & PARAM_FLAG_TIME != 0
265
+ return row[CI_P_TIME].to_date if flags & PARAM_FLAG_DATE != 0
266
+ return nil
267
+ end
268
+
269
+ def fetch_ref_value row
270
+ ref_pk = nil
271
+ instantiate ref_pk, class_by_name(row[8]) if ref_pk = row[6]
272
+ end
273
+
274
+ def object_ids objects
275
+ ids = objects.collect {|o| o.primary_key}
276
+ ids.uniq!
277
+ ids.map! {|id| id.to_s}
278
+ ids.join ', '
279
+ end
280
+
281
+ def object_ids_p objects
282
+ '(' + object_ids(objects) + ')'
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,28 @@
1
+ require 'dbi'
2
+
3
+ module Rubernate
4
+ module DBI
5
+ class MySqlRuntime < Runtime
6
+ SELECT_NEXT_PK = <<-SQL
7
+ SELECT LAST_INSERT_ID()
8
+ SQL
9
+ CREATE_PEER = <<-SQL
10
+ INSERT INTO R_OBJECTS (OBJECT_CLASS) values (?)
11
+ SQL
12
+
13
+ def initialize dbh
14
+ super
15
+ dbh.do 'SET AUTOCOMMIT=0'
16
+ end
17
+
18
+ # Creates record in r_objects for specified object
19
+ def create object
20
+ object.peer = Rubernate::Peer.new
21
+ @dbh.do CREATE_PEER, object.class.name
22
+ object.primary_key = @dbh.select_one(SELECT_NEXT_PK)[0].to_i
23
+ object.peer.dirty = true
24
+ object.primary_key
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ require 'dbi'
2
+
3
+ module Rubernate
4
+ module DBI
5
+ class OracleRuntime < Runtime
6
+ SELECT_NEXT_PK = <<-SQL
7
+ SELECT R_PK_SEQUENCE.NEXTVAL FROM DUAL
8
+ SQL
9
+ CREATE_PEER = <<-SQL
10
+ INSERT INTO R_OBJECTS (OBJECT_PK, OBJECT_CLASS) values (?, ?)
11
+ SQL
12
+
13
+ # TODO: make it work
14
+ def initialize dbh
15
+ super
16
+ dbh.do "ALTER session SET nls_date_format = 'YYYY/MM/DD HH24:MI:SS'"
17
+ end
18
+
19
+ # Creates record in r_objects for specified object
20
+ def create object
21
+ object.peer = Rubernate::Peer.new
22
+ object.primary_key = @dbh.select_one(SELECT_NEXT_PK)[0].to_i
23
+ @dbh.do CREATE_PEER, object.primary_key, object.class.name
24
+ object.peer.dirty = true
25
+ object.primary_key
26
+ end
27
+ end
28
+ end
29
+ end