iotaz 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #--
4
+ # Copyright � Peter Wood, 2005
5
+ #
6
+ # The contents of this file are subject to the Mozilla Public License Version
7
+ # 1.1 (the "License"); you may not use this file except in compliance with the
8
+ # License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.mozilla.org/MPL/
11
+ #
12
+ # Software distributed under the License is distributed on an "AS IS" basis,
13
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14
+ # the specificlanguage governing rights and limitations under the License.
15
+ #
16
+ # The Original Code is the FireRuby extension for the Ruby language.
17
+ #
18
+ # The Initial Developer of the Original Code is Peter Wood. All Rights
19
+ # Reserved.
20
+ #++
21
+ #
22
+
23
+ require 'iotaz/WorkSet'
24
+ require 'iotaz/FirebirdInterface'
25
+ require 'thread'
26
+
27
+ module Iotaz
28
+ #
29
+ # This class represents a session of interaction with the persistence
30
+ # mechanism.
31
+ #
32
+ class Session
33
+ #
34
+ # This is the constructor for the Session class.
35
+ #
36
+ # ==== Parameters
37
+ # interface:: A reference to the database interface object that the
38
+ # session will be linked to.
39
+ # listener:: A reference to an object that is interested in knowing
40
+ # if the session is terminated. This is used to allow the
41
+ # SessionFactory class to monitor available sessions. The
42
+ # object specified must implement a terminating method that
43
+ # will be invoked when the session is terminated. This
44
+ # defaults to nil to indicate no interested object.
45
+ #
46
+ def initialize(interface, listener=nil)
47
+ @interface = interface
48
+ @listener = listener
49
+ @atom = nil
50
+ end
51
+
52
+
53
+ #
54
+ # This method initializes a transaction within a session. The method
55
+ # accepts a block that delimits the lifetime of the transaction.
56
+ #
57
+ def start_transaction
58
+ @atom = Atom.new
59
+ if block_given?
60
+ begin
61
+ yield
62
+ commit
63
+ rescue
64
+ rollback
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ #
71
+ # This method invalidates a session, releasing it resources. After a call
72
+ # to this method a Session object should no longer be used.
73
+ #
74
+ def terminate
75
+ @listener.terminating(self) if @listener != nil
76
+ @interface = @atom = nil
77
+ end
78
+
79
+
80
+ #
81
+ # This method attempts to commit any outstanding transaction.
82
+ #
83
+ # ==== Exceptions
84
+ # IotazError:: Generated whenever a problem occurs committing the work
85
+ # for the transaction.
86
+ #
87
+ def commit
88
+ @atom.commit(@interface)
89
+ @atom = nil
90
+ end
91
+
92
+
93
+ #
94
+ # This method attempts to roll back any outstanding transaction.
95
+ #
96
+ # ==== Exceptions
97
+ # IotazError:: Generated whenever a problem occurs rolling back the work
98
+ # for the transaction.
99
+ #
100
+ def rollback
101
+ @atom = nil
102
+ end
103
+
104
+
105
+ #
106
+ # This method saves the details for an object to persistent store. If
107
+ # a transaction is active the actual push to the database is differed
108
+ # until a commit.
109
+ #
110
+ # ==== Parameters
111
+ # object:: A reference to the object to be saved.
112
+ #
113
+ def save(object)
114
+ metadata = object.class.iotaz_meta_data
115
+ saved = true
116
+
117
+ # Check if the object class key is nil.
118
+ metadata.keys.each do |key|
119
+ attribute = metadata.get_attribute(key)
120
+ if attribute.is_generated?
121
+ saved = false if attribute.get(object) == nil
122
+ end
123
+ break if saved == false
124
+ end
125
+
126
+ # Create the action for the save.
127
+ action = callbacks = nil
128
+ if saved
129
+ sql, callbacks, parameters = @interface.get_update_sql(object)
130
+ else
131
+ # Generate the action to save the object.
132
+ sql, callbacks, parameters = @interface.get_insert_sql(object)
133
+ end
134
+ action = Action.new(sql)
135
+ action.add_parameters(*parameters)
136
+
137
+ # Check if a transaction is active.
138
+ if @atom == nil
139
+ execute_immediate(action, *callbacks)
140
+ else
141
+ @atom.add(action, *callbacks)
142
+ end
143
+ end
144
+
145
+
146
+ #
147
+ # This method is used to load an object from the database based on its
148
+ # keys values.
149
+ #
150
+ # ==== Parameters
151
+ # klass:: The class of the object that will be loaded. This class
152
+ # must provide an iotaz_load method the creates an instance of
153
+ # the class from an data set loaded from the database.
154
+ # parameters:: An array of the parameters that for the key values to be
155
+ # used to access the specified record.
156
+ #
157
+ # ==== Exceptions
158
+ # IotazError:: Generated whenever insufficient parameters are provided,
159
+ # the select fetches more than one row or a problem occurs
160
+ # accessing the database.
161
+ #
162
+ def load(klass, *parameters)
163
+ sql = @interface.get_fetch_sql(klass)
164
+ transaction = @interface.start_transaction
165
+ begin
166
+ # Fetch the record data.
167
+ rows = @interface.execute_select(sql, parameters, transaction)
168
+ if rows.size > 1
169
+ raise IotazError.new("Error loading a '{0}' class instance. Key "\
170
+ "matches more than one row in the database.",
171
+ klass.name)
172
+ elsif rows.size == 0
173
+ raise IotazError.new("Error loading a '{0}' class instance. "\
174
+ "Object not found in database.",
175
+ klass.name)
176
+ end
177
+ @interface.commit(transaction)
178
+ transaction = nil
179
+
180
+ # Create and populate the object instance.
181
+ klass.iotaz_meta_data.populate(klass.allocate, rows[0])
182
+ ensure
183
+ @interface.rollback(transaction) if transaction != nil
184
+ end
185
+ end
186
+
187
+
188
+ #
189
+ # This method deletes the database record for an object from the database.
190
+ #
191
+ # ==== Parameters
192
+ # object:: A reference to the object to be deleted.
193
+ #
194
+ def delete(object)
195
+ metadata = object.class.iotaz_meta_data
196
+ action = Action.new(@interface.get_delete_sql(object))
197
+ action.add_parameters(*metadata.get_key_values(object))
198
+
199
+ if @atom == nil
200
+ execute_immediate(action)
201
+ else
202
+ @atom.add(action)
203
+ end
204
+ end
205
+
206
+
207
+ #
208
+ # This method executes a Query object using a Session object. The method
209
+ # returns an array of QueryRow objects.
210
+ #
211
+ # ==== Parameters
212
+ # query:: A reference to the Query object that will be executed.
213
+ # maximum:: An integer specifying the maximum number of rows to be
214
+ # retrieved. Defaults to -1 to indicate a fetch of all rows.
215
+ #
216
+ # ==== Exceptions
217
+ # IotazError:: Generated whenever a problem occurs executing the query.
218
+ #
219
+ def execute(query, maximum=-1)
220
+ rows = nil
221
+ transaction = @interface.start_transaction
222
+
223
+ begin
224
+ rows = @interface.execute_query(query, transaction, maximum)
225
+ @interface.commit(transaction)
226
+ rescue IotazError => error
227
+ @interface.rollback(transaction)
228
+ raise error
229
+ end
230
+
231
+ rows
232
+ end
233
+
234
+
235
+ #
236
+ # This method processes a single action and it's related callbacks at
237
+ # once, as opposed to making them part of a transaction. This is almost
238
+ # like using SQL with an auto-commit function.
239
+ #
240
+ # ==== Parameters
241
+ # action:: A reference to the action to be executed.
242
+ # callbacks:: An array of the callback values to be executed to.
243
+ #
244
+ def execute_immediate(action, *callbacks)
245
+ transaction = @interface.start_transaction
246
+ begin
247
+ @interface.execute_action(action, transaction)
248
+ callbacks.each do |entry|
249
+ data = @interface.execute_callback(entry, transaction)
250
+ entry.assign(data)
251
+ end
252
+ @interface.commit(transaction)
253
+ transaction = nil
254
+ rescue IotazError => error
255
+ @interface.rollback(transaction)
256
+ raise error
257
+ end
258
+ end
259
+
260
+
261
+ # Method access alterations.
262
+ private :execute_immediate
263
+ end # End of the Session class.
264
+
265
+
266
+ #
267
+ # This class represents a means of generating Session objects. The class
268
+ # helps abstract away from the concept of a database and allows for later
269
+ # expansion of the library to support other databases. The constructor for
270
+ # the class takes a hash containing configuration data as a parameter. Most
271
+ # of this data is for the database interface that will sit behind the factory
272
+ # object but an entry for the 'iotaz.database' key is used by the factory class
273
+ # to determine which database is to be used and must, therefore, be provided.
274
+ #
275
+ class SessionFactory
276
+ #
277
+ # This is the constructor for the SessionFactory class. From the details
278
+ # of the configuration data passed to it this method constructs a factory
279
+ # class instance geared to a particular database. The only database that
280
+ # is currently supported is Firebird but this may change in the future.
281
+ #
282
+ # ==== Parameters
283
+ # configuration:: A hash containing the configuration elements to be
284
+ # used by the session factory. As Firebird is the only
285
+ # currently supported RDBMS refer to the
286
+ # FirebirdInterface class for the full set of valid
287
+ # configuration parameters.
288
+ #
289
+ # ==== Exceptions
290
+ # IotazError:: Generated whenever an unknown database is specified or
291
+ # the configuration specified is incomplete.
292
+ #
293
+ def initialize(configuration)
294
+ # Create the interface class instance.
295
+ @interface = get_interface_class(configuration).new(configuration)
296
+ @sessions = []
297
+ @mutex = Mutex.new
298
+
299
+ # Clear out sessions on exit.
300
+ at_exit {@sessions.each {|session| session.terminate}}
301
+ end
302
+
303
+
304
+ #
305
+ # This method is used to have the factory object create a new Session.
306
+ #
307
+ def start
308
+ session = Session.new(@interface, self)
309
+ @mutex.synchronize {@sessions.push(session)}
310
+ session
311
+ end
312
+
313
+
314
+ #
315
+ # This method is used to terminate a session factory whenever it is no
316
+ # longer needed. This should be called to release the resources for a
317
+ # factory.
318
+ #
319
+ def shutdown
320
+ @interface.shutdown
321
+ end
322
+
323
+
324
+ #
325
+ # This method is used by the sessions produced by the factory to inform
326
+ # the factory that the session is being terminated. This method should
327
+ # not be used except in this context.
328
+ #
329
+ # ==== Parameters
330
+ # session:: A reference to the Session object being terminated.
331
+ #
332
+ def terminating(session)
333
+ @mutex.synchronize {@sessions.delete(session)}
334
+ end
335
+
336
+
337
+ #
338
+ # This method retrieves the database interface class associated with a
339
+ # given configuration.
340
+ #
341
+ # ==== Parameters
342
+ # configuration:: The configuration that will be used to determine the
343
+ # database interface class to use. This hash of values
344
+ # will be searched for an entry with the 'iotaz.database'
345
+ # key and the value of this entry used to determine the
346
+ # interface class to use.
347
+ #
348
+ # ==== Exception
349
+ # IotazError:: Generated whenever the required configuration entry is not
350
+ # present or contains a value for an unknown database.
351
+ #
352
+ def get_interface_class(configuration)
353
+ klass = nil
354
+
355
+ if configuration.key?('iotaz.database') == false
356
+ raise IotazError.new("Invalid session factory configuration. The "\
357
+ "specified configuration is missing an entry "\
358
+ "for '{0}'.", 'iotaz.database')
359
+ end
360
+
361
+ case configuration['iotaz.database'].upcase
362
+ when 'FIREBIRD' :
363
+ klass = FirebirdInterface
364
+
365
+ else
366
+ raise IotazError.new("Unknown or unsupported database specified "\
367
+ "for session factory. '{0}' is not a "\
368
+ "recognised or supported database.",
369
+ configuration['iotaz.database'])
370
+ end
371
+
372
+ klass
373
+ end
374
+
375
+
376
+ # Method access alterations.
377
+ private :get_interface_class
378
+ end # End of the SessionFactory class.
379
+ end # End of the Iotaz module.
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ #--
4
+ # Copyright � Peter Wood, 2005
5
+ #
6
+ # The contents of this file are subject to the Mozilla Public License Version
7
+ # 1.1 (the "License"); you may not use this file except in compliance with the
8
+ # License. You may obtain a copy of the License at
9
+ #
10
+ # http://www.mozilla.org/MPL/
11
+ #
12
+ # Software distributed under the License is distributed on an "AS IS" basis,
13
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14
+ # the specificlanguage governing rights and limitations under the License.
15
+ #
16
+ # The Original Code is the FireRuby extension for the Ruby language.
17
+ #
18
+ # The Initial Developer of the Original Code is Peter Wood. All Rights
19
+ # Reserved.
20
+ #++
21
+ #
22
+
23
+ require 'iotaz/IotazError'
24
+
25
+ module Iotaz
26
+ #
27
+ # This class represents a SQL statement and multiple potential parameter
28
+ # data sets for the execution of the statement. The cache attribute is
29
+ # intended for use by the database functionality to allow the Action object
30
+ # to be re-used as much as possible.
31
+ #
32
+ class Action
33
+ #
34
+ # This is the constructor for the Action class.
35
+ #
36
+ # ==== Parameters
37
+ # sql:: A string containing the SQL to be executed for the action.
38
+ #
39
+ def initialize(sql)
40
+ @sql = sql
41
+ @index = 0
42
+ @data = Array.new
43
+ @cache = nil
44
+ end
45
+
46
+
47
+ #
48
+ # This method fetches the next set of parameters from an Action object.
49
+ #
50
+ # ==== Exceptions
51
+ # IotazError:: Generated whenever the method is called on an action that
52
+ # has exhausted it's available parameter sets.
53
+ #
54
+ def get_parameters
55
+ if @index == @data.size
56
+ raise IotazError.new('Parameter set requested from exhausted action.')
57
+ end
58
+ @index += 1
59
+ @data[@index - 1]
60
+ end
61
+
62
+
63
+ #
64
+ # This method adds a new set of parameters to an Action object.
65
+ #
66
+ # ==== Parameters
67
+ # parameters:: An array containing the new parameter set.
68
+ #
69
+ def add_parameters(*parameters)
70
+ @data.push(parameters)
71
+ end
72
+
73
+
74
+ # Class attribute accessor.
75
+ attr_reader :sql, :data, :cache
76
+
77
+ # Class attribute mutator.
78
+ attr_writer :cache
79
+ end # End of the Action class.
80
+
81
+
82
+ #
83
+ # This class represents a value that must be fetched back from a database
84
+ # for a generated column. This consists of the SQL statement to fetch the
85
+ # value plus a reference to the object that the value goes into and details
86
+ # of how to get it into the object. To allow for optimizations an instance
87
+ # of the ValueCallback object may refer to more than a single object. In
88
+ # this case care should be taken with using the object. A ValueCallback that
89
+ # contains more than one object behaves differently in it's use of the
90
+ # parameters and assign methods. A call to the parameters method returns a
91
+ # parameter set for the current object and then advances it to the next
92
+ # available object. A call to assign will assign a value to the last object
93
+ # used to provide a parameter set.
94
+ #
95
+ class ValueCallback
96
+ #
97
+ # This is the constructor for the ValueCallback class.
98
+ #
99
+ # ==== Parameters
100
+ # object:: A reference to the object that the value will go back
101
+ # into.
102
+ # attribute:: The name of the attribute that the callback will be used
103
+ # to provide a value for.
104
+ # sql:: A string containing the SQL statement to fetch the object
105
+ # value.
106
+ #
107
+ def initialize(object, attribute, sql)
108
+ @objects = [object]
109
+ @current = 1
110
+ @attribute = attribute
111
+ @sql = sql
112
+ @cache = nil
113
+ end
114
+
115
+
116
+ #
117
+ # This method fetches the current object for a ValueCallback instance.
118
+ # The method does not advance the object reference.
119
+ #
120
+ def object
121
+ @objects[@current - 1]
122
+ end
123
+
124
+
125
+ #
126
+ # This method adds another object that will be covered by the callback
127
+ #
128
+ # ==== Parameters
129
+ # object:: A reference to the object that will be added to the callback.
130
+ #
131
+ def add_object(object)
132
+ @objects.push(object)
133
+ end
134
+
135
+
136
+ #
137
+ # This method assigns the generated value into the object.
138
+ #
139
+ # ==== Parameters
140
+ # value:: A reference to the generated value for the object.
141
+ #
142
+ def assign(value)
143
+ object = @objects[@current - 2]
144
+ metadata = object.class.iotaz_meta_data
145
+ metadata.get_attribute(@attribute).set(object, value)
146
+ end
147
+
148
+
149
+ #
150
+ # This method fetches the data set required to execute the SQL statement
151
+ # associated with a callback.
152
+ #
153
+ # ==== Exceptions
154
+ # IotazError:: Generated whenever any of the key attribute values for the
155
+ # object are nil.
156
+ #
157
+ def parameters
158
+ parameters = []
159
+ object = @objects[@current - 1]
160
+ @curent = @current + 1
161
+ metadata = object.class.iotaz_meta_data
162
+
163
+ metadata.keys.each do |key|
164
+ value = metadata.get_attribute(key).get(object)
165
+ if value == nil
166
+ raise IotazError.new("Value callback parameter key value is nil.")
167
+ end
168
+ parameters.push(value)
169
+ end
170
+ parameters
171
+ end
172
+
173
+
174
+ # Class attribute accessor.
175
+ attr_reader :objects, :attribute, :sql, :cache
176
+
177
+ # Class attribute mutator.
178
+ attr_writer :cache
179
+ end # End of the ValueCallback class.
180
+
181
+
182
+ #
183
+ # This class represents a series of SQL statements making up an atomic
184
+ # set of work.
185
+ #
186
+ class Atom
187
+ #
188
+ # This is the constructor for the Atom class.
189
+ #
190
+ def initialize
191
+ @actions = []
192
+ @callbacks = []
193
+ end
194
+
195
+
196
+ #
197
+ # This method adds a new action and call back set to the atom.
198
+ #
199
+ # ==== Parameters
200
+ # action:: An action reflecting the task that will be executed as
201
+ # as part of the Atom.
202
+ # callbacks:: An array of the callback that go along with the action
203
+ # being added.
204
+ #
205
+ def add(action, *callbacks)
206
+ # Store the action.
207
+ match = @actions.find {|entry| entry.sql == action.sql}
208
+ if match != nil
209
+ match.add_parameters(*action.get_parameters)
210
+ @actions.push(match)
211
+ else
212
+ @actions.push(action)
213
+ end
214
+
215
+ # Store the callbacks.
216
+ callbacks.each do |entry|
217
+ match = @callbacks.find {|callback| callback.sql == entry.sql}
218
+ if match != nil
219
+ match.add_object(entry.object)
220
+ @callbacks.push(match)
221
+ else
222
+ @callbacks.push(entry)
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+ #
229
+ # This method fetches a count of the total number of actions currently
230
+ # stored within an Atom object.
231
+ #
232
+ def action_count
233
+ @actions.size
234
+ end
235
+
236
+
237
+ #
238
+ # This method fetches a count of the total number of value callbacks
239
+ # currently stored within an Atom object.
240
+ #
241
+ def callback_count
242
+ @callbacks.size
243
+ end
244
+
245
+
246
+ #
247
+ # This method attempts to commit the work within an Atom object to the
248
+ # database.
249
+ #
250
+ # ==== Parameters
251
+ # interface:: A reference to the database interface object to be used
252
+ # to execute the work.
253
+ #
254
+ # ==== Exceptions
255
+ # IotazError:: Generated whenever a problem occurs executing the actions
256
+ # or callbacks for the Atom object.
257
+ #
258
+ def commit(interface)
259
+ transaction = nil
260
+ begin
261
+ # Start a transaction and execute the actions.
262
+ transaction = interface.start_transaction
263
+ @actions.each do |entry|
264
+ interface.execute_action(entry, transaction)
265
+ end
266
+
267
+ # Execute the value callbacks to update objects.
268
+ @callbacks.each do |entry|
269
+ entry.assign(interface.execute_callback(entry, transaction))
270
+ end
271
+
272
+ # Clean up cached items.
273
+ items = []
274
+ @actions.each do |entry|
275
+ items.push(entry.cache) if entry.cache != nil
276
+ end
277
+ @callbacks.each do |entry|
278
+ items.push(entry.cache) if entry.cache != nil
279
+ end
280
+ interface.clean_cache(items)
281
+
282
+ # Commit the transaction.
283
+ interface.commit(transaction)
284
+ transaction = nil
285
+ rescue IotazError => error
286
+ raise error
287
+ rescue Exception => error
288
+ raise IotazError.new("Error committing work atom, Cause: {0}",
289
+ error.message)
290
+ ensure
291
+ interface.rollback(transaction) if transaction != nil
292
+ end
293
+ end
294
+ end # End of the Atom class.
295
+ end # End of the Iotaz module.