lafcadio 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -76,18 +76,18 @@ module Lafcadio
76
76
  end
77
77
 
78
78
  class DbBridge #:nodoc:
79
- @@dbh = nil
80
79
  @@last_pk_id_inserted = nil
81
80
 
82
81
  def self._load(aString)
83
- aString =~ /dbh:/
84
- dbString = $'
82
+ aString =~ /db_conn:/
83
+ db_conn_str = $'
85
84
  begin
86
- dbh = Marshal.load(dbString)
85
+ db_conn = Marshal.load db_conn_str
87
86
  rescue TypeError
88
- dbh = nil
87
+ db_conn = nil
89
88
  end
90
- new dbh
89
+ DbConnection.set_db_connection db_conn
90
+ new
91
91
  end
92
92
 
93
93
  def initialize
@@ -131,10 +131,7 @@ module Lafcadio
131
131
  end
132
132
 
133
133
  def group_query( query )
134
- execute_select( query.to_sql )[0].collect { |val|
135
- a_field = query.domain_class.get_field( query.field_name )
136
- a_field.value_from_sql( val )
137
- }
134
+ execute_select( query.to_sql ).map { |row| query.result_row( row ) }
138
135
  end
139
136
 
140
137
  def last_pk_id_inserted; @@last_pk_id_inserted; end
@@ -150,6 +147,34 @@ module Lafcadio
150
147
  sqllog.info sql
151
148
  end
152
149
  end
150
+
151
+ def transaction( action )
152
+ tr = Transaction.new @db_conn
153
+ tr.commit
154
+ begin
155
+ action.call tr
156
+ tr.commit
157
+ rescue RollbackError
158
+ # rollback handled by Transaction
159
+ rescue
160
+ err_to_raise = $!
161
+ tr.rollback false
162
+ raise err_to_raise
163
+ end
164
+ end
165
+
166
+ class Transaction
167
+ def initialize( db_conn ); @db_conn = db_conn; end
168
+
169
+ def commit; @db_conn.commit; end
170
+
171
+ def rollback( raise_error = true )
172
+ @db_conn.rollback
173
+ raise RollbackError if raise_error
174
+ end
175
+ end
176
+
177
+ class RollbackError < StandardError; end
153
178
  end
154
179
 
155
180
  class DbConnection < ContextualService::Service
@@ -186,6 +211,8 @@ module Lafcadio
186
211
  end
187
212
  @@dbh = @@connectionClass.connect( dbAndHost, config['dbuser'],
188
213
  config['dbpassword'] )
214
+ @@dbh['AutoCommit'] = false
215
+ @@dbh
189
216
  end
190
217
 
191
218
  def method_missing( symbol, *args )
@@ -280,7 +307,7 @@ module Lafcadio
280
307
  value = @obj.send(field.name)
281
308
  unless field.db_will_automatically_write
282
309
  nameValues << field.name_for_sql
283
- nameValues << field.value_for_sql(value)
310
+ nameValues <<(field.value_for_sql(value))
284
311
  end
285
312
  if field.bind_write?
286
313
  @bind_values << value
@@ -418,7 +445,6 @@ module Lafcadio
418
445
  # myDomainObject.commit
419
446
  def commit(db_object)
420
447
  @cache.commit( db_object )
421
- db_object.reset_original_values_hash
422
448
  db_object
423
449
  end
424
450
 
@@ -487,7 +513,8 @@ module Lafcadio
487
513
  # ObjectStore#get_max( Invoice, "rate" )
488
514
  # will return the highest rate for all invoices.
489
515
  def get_max( domain_class, field_name = 'pk_id' )
490
- @dbBridge.group_query( Query::Max.new( domain_class, field_name ) ).only
516
+ qry = Query::Max.new( domain_class, field_name )
517
+ @dbBridge.group_query( qry ).only[:max]
491
518
  end
492
519
 
493
520
  # Retrieves a collection of domain objects by +pk_id+.
@@ -528,6 +555,8 @@ module Lafcadio
528
555
  def mock? #:nodoc:
529
556
  false
530
557
  end
558
+
559
+ def query( query ); @dbBridge.group_query( query ); end
531
560
 
532
561
  def respond_to?( symbol, include_private = false )
533
562
  begin
@@ -537,6 +566,8 @@ module Lafcadio
537
566
  end
538
567
  end
539
568
 
569
+ def transaction( &action ); @dbBridge.transaction( action ); end
570
+
540
571
  class Cache #:nodoc:
541
572
  def initialize( dbBridge )
542
573
  @dbBridge = dbBridge
@@ -583,21 +614,11 @@ module Lafcadio
583
614
  query.implies?( other_query )
584
615
  }
585
616
  if pk_ids
586
- collection = ( pk_ids.collect { |pk_id|
617
+ @collections_by_query[query] = ( pk_ids.collect { |pk_id|
587
618
  get( query.domain_class, pk_id )
588
619
  } ).select { |dobj| query.object_meets( dobj ) }.collect { |dobj|
589
620
  dobj.pk_id
590
621
  }
591
- collection = collection[query.limit] if query.limit
592
- if ( order_by = query.order_by )
593
- collection = collection.sort_by { |pk_id|
594
- get( query.domain_class, pk_id ).send( order_by )
595
- }
596
- collection.reverse! if query.order_by_order == Query::DESC
597
- else
598
- collection = collection.sort
599
- end
600
- @collections_by_query[query] = collection
601
622
  elsif @collections_by_query.values
602
623
  newObjects = @dbBridge.get_collection_by_query(query)
603
624
  newObjects.each { |dbObj| save dbObj }
@@ -0,0 +1,766 @@
1
+ require 'dbi'
2
+ require 'lafcadio/domain'
3
+ require 'lafcadio/query'
4
+ require 'lafcadio/util'
5
+
6
+ module Lafcadio
7
+ class Committer #:nodoc:
8
+ INSERT = 1
9
+ UPDATE = 2
10
+ DELETE = 3
11
+
12
+ attr_reader :commit_type, :db_object
13
+
14
+ def initialize(db_object, dbBridge)
15
+ @db_object = db_object
16
+ @dbBridge = dbBridge
17
+ @objectStore = ObjectStore.get_object_store
18
+ @commit_type = nil
19
+ end
20
+
21
+ def execute
22
+ @db_object.verify if LafcadioConfig.new()['checkFields'] == 'onCommit'
23
+ set_commit_type
24
+ @db_object.last_commit = get_last_commit
25
+ @db_object.pre_commit_trigger
26
+ update_dependent_domain_objects if @db_object.delete
27
+ @dbBridge.commit @db_object
28
+ unless @db_object.pk_id
29
+ @db_object.pk_id = @dbBridge.last_pk_id_inserted
30
+ end
31
+ @db_object.post_commit_trigger
32
+ end
33
+
34
+ def get_last_commit
35
+ if @db_object.delete
36
+ DomainObject::COMMIT_DELETE
37
+ elsif @db_object.pk_id
38
+ DomainObject::COMMIT_EDIT
39
+ else
40
+ DomainObject::COMMIT_ADD
41
+ end
42
+ end
43
+
44
+ def set_commit_type
45
+ if @db_object.delete
46
+ @commit_type = DELETE
47
+ elsif @db_object.pk_id
48
+ @commit_type = UPDATE
49
+ else
50
+ @commit_type = INSERT
51
+ end
52
+ end
53
+
54
+ def update_dependent_domain_objects
55
+ dependent_classes = @db_object.domain_class.dependent_classes
56
+ dependent_classes.keys.each { |aClass|
57
+ field = dependent_classes[aClass]
58
+ collection = @objectStore.get_filtered( aClass.name, @db_object,
59
+ field.name )
60
+ collection.each { |dependentObject|
61
+ if field.delete_cascade
62
+ dependentObject.delete = true
63
+ else
64
+ dependentObject.send( field.name + '=', nil )
65
+ end
66
+ @objectStore.commit(dependentObject)
67
+ }
68
+ }
69
+ end
70
+ end
71
+
72
+ class CouldntMatchDomainClassError < RuntimeError #:nodoc:
73
+ end
74
+
75
+ class DbBridge #:nodoc:
76
+ @@dbh = nil
77
+ @@last_pk_id_inserted = nil
78
+
79
+ def self._load(aString)
80
+ aString =~ /dbh:/
81
+ dbString = $'
82
+ begin
83
+ dbh = Marshal.load(dbString)
84
+ rescue TypeError
85
+ dbh = nil
86
+ end
87
+ new dbh
88
+ end
89
+
90
+ def initialize
91
+ @db_conn = DbConnection.get_db_connection
92
+ ObjectSpace.define_finalizer( self, proc { |id|
93
+ DbConnection.get_db_connection.disconnect
94
+ } )
95
+ end
96
+
97
+ def _dump(aDepth)
98
+ dbDump = @dbh.respond_to?( '_dump' ) ? @dbh._dump : @dbh.class.to_s
99
+ "dbh:#{dbDump}"
100
+ end
101
+
102
+ def commit(db_object)
103
+ sqlMaker = DomainObjectSqlMaker.new(db_object)
104
+ sqlMaker.sql_statements.each { |sql, binds| execute_commit( sql, binds ) }
105
+ if sqlMaker.sql_statements[0].first =~ /insert/
106
+ sql = 'select last_insert_id()'
107
+ result = execute_select( sql )
108
+ @@last_pk_id_inserted = result[0]['last_insert_id()'].to_i
109
+ end
110
+ end
111
+
112
+ def execute_commit( sql, binds ); @db_conn.do( sql, *binds ); end
113
+
114
+ def execute_select(sql)
115
+ maybe_log sql
116
+ begin
117
+ @db_conn.select_all( sql )
118
+ rescue DBI::DatabaseError => e
119
+ raise $!.to_s + ": #{ e.errstr }"
120
+ end
121
+ end
122
+
123
+ def get_collection_by_query(query)
124
+ domain_class = query.domain_class
125
+ execute_select( query.to_sql ).collect { |row_hash|
126
+ domain_class.new( SqlValueConverter.new( domain_class, row_hash ) )
127
+ }
128
+ end
129
+
130
+ def group_query( query )
131
+ execute_select( query.to_sql )[0].collect { |val|
132
+ if query.field_name != query.domain_class.sql_primary_key_name
133
+ a_field = query.domain_class.get_field( query.field_name )
134
+ a_field.value_from_sql( val )
135
+ else
136
+ val.to_i
137
+ end
138
+ }
139
+ end
140
+
141
+ def last_pk_id_inserted; @@last_pk_id_inserted; end
142
+
143
+ def maybe_log(sql)
144
+ config = LafcadioConfig.new
145
+ if config['logSql'] == 'y'
146
+ sqllog = Log4r::Logger['sql'] || Log4r::Logger.new( 'sql' )
147
+ filename = File.join( config['logdir'], config['sqlLogFile'] || 'sql' )
148
+ outputter = Log4r::FileOutputter.new( 'outputter',
149
+ { :filename => filename } )
150
+ sqllog.outputters = outputter
151
+ sqllog.info sql
152
+ end
153
+ end
154
+ end
155
+
156
+ class DbConnection < ContextualService
157
+ @@connectionClass = DBI
158
+ @@db_name = nil
159
+ @@dbh = nil
160
+
161
+ def self.flush
162
+ DbConnection.set_db_connection( nil )
163
+ @@dbh = nil
164
+ end
165
+
166
+ def self.set_connection_class( aClass ); @@connectionClass = aClass; end
167
+
168
+ def self.set_db_name( db_name ); @@db_name = db_name; end
169
+
170
+ def self.set_dbh( dbh ); @@dbh = dbh; end
171
+
172
+ def initialize
173
+ @@dbh = load_new_dbh if @@dbh.nil?
174
+ @dbh = @@dbh
175
+ end
176
+
177
+ def disconnect; @dbh.disconnect if @dbh; end
178
+
179
+ def load_new_dbh
180
+ config = LafcadioConfig.new
181
+ dbName = @@db_name || config['dbname']
182
+ dbAndHost = nil
183
+ if dbName && config['dbhost']
184
+ dbAndHost = "dbi:Mysql:#{ dbName }:#{ config['dbhost'] }"
185
+ else
186
+ dbAndHost = "dbi:#{config['dbconn']}"
187
+ end
188
+ @@dbh = @@connectionClass.connect( dbAndHost, config['dbuser'],
189
+ config['dbpassword'] )
190
+ end
191
+
192
+ def method_missing( symbol, *args )
193
+ @dbh.send( symbol, *args )
194
+ end
195
+ end
196
+
197
+ class DomainObjectInitError < RuntimeError #:nodoc:
198
+ attr_reader :messages
199
+
200
+ def initialize(messages)
201
+ @messages = messages
202
+ end
203
+ end
204
+
205
+ class DomainObjectNotFoundError < RuntimeError #:nodoc:
206
+ end
207
+
208
+ # The DomainObjectProxy is used when retrieving domain objects that are
209
+ # linked to other domain objects with LinkFields. In terms of +domain_class+
210
+ # and
211
+ # +pk_id+, a DomainObjectProxy instance looks to the outside world like the
212
+ # domain object it's supposed to represent. It only retrieves its domain
213
+ # object from the database when member data is requested.
214
+ #
215
+ # In normal usage you will probably never manipulate a DomainObjectProxy
216
+ # directly, but you may discover it by accident by calling
217
+ # DomainObjectProxy#class (or DomainObject#class) instead of
218
+ # DomainObjectProxy#domain_class (or DomainObjectProxy#domain_class).
219
+ class DomainObjectProxy
220
+ include DomainComparable
221
+
222
+ attr_accessor :domain_class, :pk_id
223
+
224
+ def initialize(domain_classOrDbObject, pk_id = nil)
225
+ if pk_id
226
+ @domain_class = domain_classOrDbObject
227
+ @pk_id = pk_id
228
+ elsif domain_classOrDbObject.class < DomainObject
229
+ @db_object = domain_classOrDbObject
230
+ @d_obj_retrieve_time = Time.now
231
+ @domain_class = @db_object.class
232
+ @pk_id = @db_object.pk_id
233
+ else
234
+ raise ArgumentError
235
+ end
236
+ @db_object = nil
237
+ end
238
+
239
+ def get_db_object
240
+ object_store = ObjectStore.get_object_store
241
+ if @db_object.nil? || needs_refresh?
242
+ @db_object = object_store.get( @domain_class, @pk_id )
243
+ @d_obj_retrieve_time = Time.now
244
+ end
245
+ @db_object
246
+ end
247
+
248
+ def hash
249
+ get_db_object.hash
250
+ end
251
+
252
+ def method_missing(methodId, *args)
253
+ get_db_object.send(methodId.id2name, *args)
254
+ end
255
+
256
+ def needs_refresh?
257
+ object_store = ObjectStore.get_object_store
258
+ last_commit_time = object_store.last_commit_time( @domain_class, @pk_id )
259
+ !last_commit_time.nil? && last_commit_time > @d_obj_retrieve_time
260
+ end
261
+
262
+ def to_s
263
+ get_db_object.to_s
264
+ end
265
+ end
266
+
267
+ class DomainObjectSqlMaker #:nodoc:
268
+ attr_reader :bind_values
269
+
270
+ def initialize(obj); @obj = obj; end
271
+
272
+ def delete_sql( domain_class )
273
+ "delete from #{ domain_class.table_name} " +
274
+ "where #{ domain_class.sql_primary_key_name }=#{ @obj.pk_id }"
275
+ end
276
+
277
+ def get_name_value_pairs( domain_class )
278
+ nameValues = []
279
+ domain_class.class_fields.each { |field|
280
+ unless field.instance_of?( PrimaryKeyField )
281
+ value = @obj.send(field.name)
282
+ unless field.db_will_automatically_write
283
+ nameValues << field.name_for_sql
284
+ nameValues <<(field.value_for_sql(value))
285
+ end
286
+ if field.bind_write?
287
+ @bind_values << value
288
+ end
289
+ end
290
+ }
291
+ QueueHash.new( *nameValues )
292
+ end
293
+
294
+ def insert_sql( domain_class )
295
+ fields = domain_class.class_fields
296
+ nameValuePairs = get_name_value_pairs( domain_class )
297
+ if domain_class.is_based_on?
298
+ nameValuePairs[domain_class.sql_primary_key_name] = 'LAST_INSERT_ID()'
299
+ end
300
+ fieldNameStr = nameValuePairs.keys.join ", "
301
+ fieldValueStr = nameValuePairs.values.join ", "
302
+ "insert into #{ domain_class.table_name}(#{fieldNameStr}) " +
303
+ "values(#{fieldValueStr})"
304
+ end
305
+
306
+ def sql_statements
307
+ statements = []
308
+ if @obj.error_messages.size > 0
309
+ raise DomainObjectInitError, @obj.error_messages, caller
310
+ end
311
+ @obj.class.self_and_concrete_superclasses.each { |domain_class|
312
+ statements << statement_bind_value_pair( domain_class )
313
+ }
314
+ statements.reverse
315
+ end
316
+
317
+ def statement_bind_value_pair( domain_class )
318
+ @bind_values = []
319
+ if @obj.pk_id == nil
320
+ statement = insert_sql( domain_class )
321
+ else
322
+ if @obj.delete
323
+ statement = delete_sql( domain_class )
324
+ else
325
+ statement = update_sql( domain_class)
326
+ end
327
+ end
328
+ [statement, @bind_values]
329
+ end
330
+
331
+ def update_sql( domain_class )
332
+ nameValueStrings = []
333
+ nameValuePairs = get_name_value_pairs( domain_class )
334
+ nameValuePairs.each { |key, value|
335
+ nameValueStrings << "#{key}=#{ value }"
336
+ }
337
+ allNameValues = nameValueStrings.join ', '
338
+ "update #{ domain_class.table_name} set #{allNameValues} " +
339
+ "where #{ domain_class.sql_primary_key_name}=#{@obj.pk_id}"
340
+ end
341
+ end
342
+
343
+ class FieldMatchError < StandardError; end
344
+
345
+ # The ObjectStore represents the database in a Lafcadio application.
346
+ #
347
+ # = Configuring the ObjectStore
348
+ # The ObjectStore depends on a few values being set correctly in the
349
+ # LafcadioConfig file:
350
+ # [dbuser] The database username.
351
+ # [dbpassword] The database password.
352
+ # [dbname] The database name.
353
+ # [dbhost] The database host.
354
+ #
355
+ # = Instantiating ObjectStore
356
+ # The ObjectStore is a ContextualService, meaning you can't get an instance by
357
+ # calling ObjectStore.new. Instead, you should call
358
+ # ObjectStore.get_object_store. (Using a ContextualService makes it easier to
359
+ # make out the ObjectStore for unit tests: See ContextualService for more.)
360
+ #
361
+ # = Dynamic method calls
362
+ # ObjectStore uses reflection to provide a lot of convenience methods for
363
+ # querying domain objects in a number of ways.
364
+ # [ObjectStore#get< domain class > (pk_id)]
365
+ # Retrieves one domain object by pk_id. For example,
366
+ # ObjectStore#getUser( 100 )
367
+ # will return User 100.
368
+ # [ObjectStore#get< domain class >s (searchTerm, fieldName = nil)]
369
+ # Returns a collection of all instances of that domain class matching that
370
+ # search term. For example,
371
+ # ObjectStore#getProducts( aProductCategory )
372
+ # queries MySQL for all products that belong to that product category. You
373
+ # can omit +fieldName+ if +searchTerm+ is a non-nil domain object, and the
374
+ # field connecting the first domain class to the second is named after the
375
+ # domain class. (For example, the above line assumes that Product has a
376
+ # field named "productCategory".) Otherwise, it's best to include
377
+ # +fieldName+:
378
+ # ObjectStore#getUsers( "Jones", "lastName" )
379
+ #
380
+ # = Querying
381
+ # ObjectStore can also be used to generate complex, ad-hoc queries which
382
+ # emulate much of the functionality you'd get from writing the SQL yourself.
383
+ # Furthermore, these queries can be run against in-memory data stores, which
384
+ # is particularly useful for tests.
385
+ # date = Date.new( 2003, 1, 1 )
386
+ # ObjectStore#getInvoices { |invoice|
387
+ # Query.And( invoice.date.gte( date ), invoice.rate.equals( 10 ),
388
+ # invoice.hours.equals( 10 ) )
389
+ # }
390
+ # is the same as
391
+ # select * from invoices
392
+ # where (date >= '2003-01-01' and rate = 10 and hours = 10)
393
+ # See lafcadio/query.rb for more.
394
+ #
395
+ # = SQL Logging
396
+ # Lafcadio uses log4r to log all of its SQL statements. The simplest way to
397
+ # turn on logging is to set the following values in the LafcadioConfig file:
398
+ # [logSql] Should be set to "y" to turn on logging.
399
+ # [logdir] The directory where log files should be written. Required if
400
+ # +logSql+ is "y"
401
+ # [sqlLogFile] The name of the file (not including its directory) where SQL
402
+ # should be logged. Default is "sql".
403
+ #
404
+ # = Triggers
405
+ # Domain classes can be set to fire triggers either before or after commits.
406
+ # Since these triggers are executed in Ruby, they're easy to test. See
407
+ # DomainObject#pre_commit_trigger and DomainObject#post_commit_trigger for more.
408
+ class ObjectStore < ContextualService
409
+ def self.set_db_name(dbName) #:nodoc:
410
+ DbConnection.set_db_name dbName
411
+ end
412
+
413
+ def initialize( dbBridge = nil ) #:nodoc:
414
+ @dbBridge = dbBridge == nil ? DbBridge.new : dbBridge
415
+ @cache = ObjectStore::Cache.new( @dbBridge )
416
+ end
417
+
418
+ # Commits a domain object to the database. You can also simply call
419
+ # myDomainObject.commit
420
+ def commit(db_object)
421
+ @cache.commit( db_object )
422
+ db_object
423
+ end
424
+
425
+ # Flushes one domain object from its cache.
426
+ def flush(db_object)
427
+ @cache.flush db_object
428
+ end
429
+
430
+ # Returns the domain object corresponding to the domain class and pk_id.
431
+ def get( domain_class, pk_id )
432
+ query = Query.new domain_class, pk_id
433
+ @cache.get_by_query( query )[0] ||
434
+ ( raise( DomainObjectNotFoundError,
435
+ "Can't find #{domain_class} #{pk_id}", caller ) )
436
+ end
437
+
438
+ # Returns all domain objects for the given domain class.
439
+ def get_all(domain_class); @cache.get_by_query( Query.new( domain_class ) ); end
440
+
441
+ # Returns the DbBridge; this is useful in case you need to use raw SQL for a
442
+ # specific query.
443
+ def get_db_bridge; @dbBridge; end
444
+
445
+ def get_field_name( domain_object )
446
+ domain_object.domain_class.basename.decapitalize
447
+ end
448
+
449
+ def get_filtered(domain_class_name, searchTerm, fieldName = nil) #:nodoc:
450
+ domain_class = DomainObject.get_domain_class_from_string(
451
+ domain_class_name
452
+ )
453
+ fieldName = get_field_name( searchTerm ) unless fieldName
454
+ get_subset( Query::Equals.new( fieldName, searchTerm, domain_class ) )
455
+ end
456
+
457
+ def get_map_match( domain_class, mapped ) #:nodoc:
458
+ Query::Equals.new( get_field_name( mapped ), mapped, domain_class )
459
+ end
460
+
461
+ def get_map_object( domain_class, map1, map2 ) #:nodoc:
462
+ unless map1 && map2
463
+ raise ArgumentError,
464
+ "ObjectStore#get_map_object needs two non-nil keys", caller
465
+ end
466
+ mapMatch1 = get_map_match domain_class, map1
467
+ mapMatch2 = get_map_match domain_class, map2
468
+ condition = Query::CompoundCondition.new mapMatch1, mapMatch2
469
+ get_subset(condition)[0]
470
+ end
471
+
472
+ def get_mapped(searchTerm, resultTypeName) #:nodoc:
473
+ resultType = DomainObject.get_domain_class_from_string resultTypeName
474
+ firstTypeName = searchTerm.class.basename
475
+ secondTypeName = resultType.basename
476
+ mapTypeName = firstTypeName + secondTypeName
477
+ get_filtered( mapTypeName, searchTerm ).collect { |mapObj|
478
+ mapObj.send( resultType.name.decapitalize )
479
+ }
480
+ end
481
+
482
+ # Retrieves the maximum value across all instances of one domain class.
483
+ # ObjectStore#get_max( Client )
484
+ # returns the highest +pk_id+ in the +clients+ table.
485
+ # ObjectStore#get_max( Invoice, "rate" )
486
+ # will return the highest rate for all invoices.
487
+ def get_max( domain_class, field_name = 'pk_id' )
488
+ @dbBridge.group_query( Query::Max.new( domain_class, field_name ) ).only
489
+ end
490
+
491
+ # Retrieves a collection of domain objects by +pk_id+.
492
+ # ObjectStore#get_objects( Clients, [ 1, 2, 3 ] )
493
+ def get_objects( domain_class, pk_ids )
494
+ if pk_ids.is_a?( Array ) && pk_ids.all? { |elt| elt.is_a?( Integer ) }
495
+ get_subset Query::In.new( 'pk_id', pk_ids, domain_class )
496
+ else
497
+ raise(
498
+ ArgumentError,
499
+ "ObjectStore#get_objects( domain_class, pk_ids ): pk_ids needs to " +
500
+ "be an array of integers",
501
+ caller
502
+ )
503
+ end
504
+ end
505
+
506
+ def get_subset(conditionOrQuery) #:nodoc:
507
+ if conditionOrQuery.class <= Query::Condition
508
+ condition = conditionOrQuery
509
+ query = Query.new condition.domain_class, condition
510
+ else
511
+ query = conditionOrQuery
512
+ end
513
+ @cache.get_by_query( query )
514
+ end
515
+
516
+ def last_commit_time( domain_class, pk_id ) #:nodoc:
517
+ @cache.last_commit_time( domain_class, pk_id )
518
+ end
519
+
520
+ def method_missing(methodId, *args) #:nodoc:
521
+ proc = block_given? ? ( proc { |obj| yield( obj ) } ) : nil
522
+ dispatch = MethodDispatch.new( methodId, proc, *args )
523
+ self.send( dispatch.symbol, *dispatch.args )
524
+ end
525
+
526
+ def respond_to?( symbol, include_private = false )
527
+ begin
528
+ dispatch = MethodDispatch.new( symbol )
529
+ rescue NoMethodError
530
+ super
531
+ end
532
+ end
533
+
534
+ class Cache #:nodoc:
535
+ def initialize( dbBridge )
536
+ @dbBridge = dbBridge
537
+ @objects = {}
538
+ @collections_by_query = {}
539
+ @commit_times = {}
540
+ end
541
+
542
+ def commit( db_object )
543
+ committer = Committer.new db_object, @dbBridge
544
+ committer.execute
545
+ update_after_commit( committer )
546
+ end
547
+
548
+ # Flushes a domain object.
549
+ def flush(db_object)
550
+ hash_by_domain_class( db_object.domain_class ).delete db_object.pk_id
551
+ flush_collection_cache( db_object.domain_class )
552
+ end
553
+
554
+ def flush_collection_cache( domain_class )
555
+ @collections_by_query.keys.each { |query|
556
+ if query.domain_class == domain_class
557
+ @collections_by_query.delete( query )
558
+ end
559
+ }
560
+ end
561
+
562
+ # Returns a cached domain object, or nil if none is found.
563
+ def get( domain_class, pk_id )
564
+ hash_by_domain_class( domain_class )[pk_id].clone
565
+ end
566
+
567
+ # Returns an array of all domain objects of a given type.
568
+ def get_all( domain_class )
569
+ hash_by_domain_class( domain_class ).values.collect { |d_obj|
570
+ d_obj.clone
571
+ }
572
+ end
573
+
574
+ def get_by_query( query )
575
+ unless @collections_by_query[query]
576
+ superset_query, pk_ids =
577
+ @collections_by_query.find { |other_query, pk_ids|
578
+ query.implies?( other_query )
579
+ }
580
+ if pk_ids
581
+ @collections_by_query[query] = ( pk_ids.collect { |pk_id|
582
+ get( query.domain_class, pk_id )
583
+ } ).select { |dobj| query.object_meets( dobj ) }.collect { |dobj|
584
+ dobj.pk_id
585
+ }
586
+ elsif @collections_by_query.values
587
+ newObjects = @dbBridge.get_collection_by_query(query)
588
+ newObjects.each { |dbObj| save dbObj }
589
+ @collections_by_query[query] = newObjects.collect { |dobj|
590
+ dobj.pk_id
591
+ }
592
+ end
593
+ end
594
+ collection = []
595
+ @collections_by_query[query].each { |pk_id|
596
+ dobj = get( query.domain_class, pk_id )
597
+ collection << dobj if dobj
598
+ }
599
+ collection
600
+ end
601
+
602
+ def hash_by_domain_class( domain_class )
603
+ unless @objects[domain_class]
604
+ @objects[domain_class] = {}
605
+ end
606
+ @objects[domain_class]
607
+ end
608
+
609
+ def last_commit_time( domain_class, pk_id )
610
+ by_domain_class = @commit_times[domain_class]
611
+ by_domain_class ? by_domain_class[pk_id] : nil
612
+ end
613
+
614
+ def set_commit_time( d_obj )
615
+ by_domain_class = @commit_times[d_obj.domain_class]
616
+ if by_domain_class.nil?
617
+ by_domain_class = {}
618
+ @commit_times[d_obj.domain_class] = by_domain_class
619
+ end
620
+ by_domain_class[d_obj.pk_id] = Time.now
621
+ end
622
+
623
+ # Saves a domain object.
624
+ def save(db_object)
625
+ hash = hash_by_domain_class( db_object.domain_class )
626
+ hash[db_object.pk_id] = db_object
627
+ flush_collection_cache( db_object.domain_class )
628
+ end
629
+
630
+ def update_after_commit( committer ) #:nodoc:
631
+ if committer.commit_type == Committer::UPDATE ||
632
+ committer.commit_type == Committer::INSERT
633
+ save( committer.db_object )
634
+ elsif committer.commit_type == Committer::DELETE
635
+ flush( committer.db_object )
636
+ end
637
+ set_commit_time( committer.db_object )
638
+ end
639
+ end
640
+
641
+ class MethodDispatch #:nodoc:
642
+ attr_reader :symbol, :args
643
+
644
+ def initialize( orig_method, *other_args )
645
+ @orig_method = orig_method
646
+ @orig_args = other_args
647
+ if @orig_args.size > 0
648
+ @maybe_proc = @orig_args.shift
649
+ end
650
+ @methodName = orig_method.id2name
651
+ if @methodName =~ /^get(.*)$/
652
+ dispatch_get_method
653
+ else
654
+ raise_no_method_error
655
+ end
656
+ end
657
+
658
+ def dispatch_get_plural
659
+ if @orig_args.size == 0 && @maybe_proc.nil?
660
+ @symbol = :get_all
661
+ @args = [ @domain_class ]
662
+ else
663
+ searchTerm = @orig_args[0]
664
+ fieldName = @orig_args[1]
665
+ if searchTerm.nil? && @maybe_proc.nil? && fieldName.nil?
666
+ msg = "ObjectStore\##{ @orig_method } needs a field name as its " +
667
+ "second argument if its first argument is nil"
668
+ raise( ArgumentError, msg, caller )
669
+ end
670
+ dispatch_get_plural_by_query_block_or_search_term( searchTerm,
671
+ fieldName )
672
+ end
673
+ end
674
+
675
+ def dispatch_get_plural_by_query_block
676
+ inferrer = Query::Inferrer.new( @domain_class ) { |obj|
677
+ @maybe_proc.call( obj )
678
+ }
679
+ @symbol = :get_subset
680
+ @args = [ inferrer.execute ]
681
+ end
682
+
683
+ def dispatch_get_plural_by_query_block_or_search_term( searchTerm,
684
+ fieldName )
685
+ if !@maybe_proc.nil? && searchTerm.nil?
686
+ dispatch_get_plural_by_query_block
687
+ elsif @maybe_proc.nil? && ( !( searchTerm.nil? && fieldName.nil? ) )
688
+ @symbol = :get_filtered
689
+ @args = [ @domain_class.name, searchTerm, fieldName ]
690
+ else
691
+ raise( ArgumentError,
692
+ "Shouldn't send both a query block and a search term",
693
+ caller )
694
+ end
695
+ end
696
+
697
+ def dispatch_get_method
698
+ begin
699
+ dispatch_get_singular
700
+ rescue CouldntMatchDomainClassError
701
+ domain_class_name = English.singular(
702
+ camel_case_method_name_after_get
703
+ )
704
+ begin
705
+ @domain_class =
706
+ DomainObject.get_domain_class_from_string( domain_class_name )
707
+ dispatch_get_plural
708
+ rescue CouldntMatchDomainClassError
709
+ raise_no_method_error
710
+ end
711
+ end
712
+ end
713
+
714
+ def dispatch_get_singular
715
+ domain_class = DomainObject.get_domain_class_from_string(
716
+ camel_case_method_name_after_get
717
+ )
718
+ if @orig_args[0].class <= Integer
719
+ @symbol = :get
720
+ @args = [ domain_class, @orig_args[0] ]
721
+ elsif @orig_args[0].class <= DomainObject
722
+ @symbol = :get_map_object
723
+ @args = [ domain_class, @orig_args[0], @orig_args[1] ]
724
+ end
725
+ end
726
+
727
+ def camel_case_method_name_after_get
728
+ @orig_method.id2name =~ /^get(.*)$/
729
+ $1.underscore_to_camel_case
730
+ end
731
+
732
+ def raise_no_method_error
733
+ raise( NoMethodError, "undefined method '#{ @methodName }'", caller )
734
+ end
735
+ end
736
+ end
737
+
738
+ class SqlValueConverter #:nodoc:
739
+ attr_reader :domain_class, :row_hash
740
+
741
+ def initialize( domain_class, row_hash )
742
+ @domain_class = domain_class
743
+ @row_hash = row_hash
744
+ end
745
+
746
+ def []( key )
747
+ begin
748
+ field = @domain_class.get_field( key )
749
+ val = field.value_from_sql( @row_hash[ field.db_field_name ] )
750
+ if field.instance_of?( PrimaryKeyField ) && val.nil?
751
+ raise FieldMatchError, error_msg, caller
752
+ else
753
+ val
754
+ end
755
+ rescue MissingError
756
+ nil
757
+ end
758
+ end
759
+
760
+ def error_msg
761
+ "The field \"" + @domain_class.sql_primary_key_name +
762
+ "\" can\'t be found in the table \"" +
763
+ @domain_class.table_name + "\"."
764
+ end
765
+ end
766
+ end