Rubernate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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