lafcadio 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,7 @@ require 'lafcadio/depend'
3
3
  require 'lafcadio/domain'
4
4
  require 'lafcadio/query'
5
5
  require 'lafcadio/util'
6
+ require 'monitor'
6
7
 
7
8
  module Lafcadio
8
9
  class DomainObjectInitError < RuntimeError #:nodoc:
@@ -42,11 +43,16 @@ module Lafcadio
42
43
 
43
44
  def db_object #:nodoc:
44
45
  if @db_object.nil? || needs_refresh?
45
- @db_object = ObjectStore.get_object_store.get( @domain_class, @pk_id )
46
- @d_obj_retrieve_time = Time.now
46
+ dbo = ObjectStore.get_object_store.get( @domain_class, @pk_id )
47
+ self.db_object = dbo
47
48
  end
48
49
  @db_object
49
50
  end
51
+
52
+ def db_object=( dbo )
53
+ @db_object = dbo
54
+ @d_obj_retrieve_time = Time.now
55
+ end
50
56
 
51
57
  def hash #:nodoc:
52
58
  db_object.hash
@@ -141,10 +147,8 @@ module Lafcadio
141
147
  # DomainObject#pre_commit_trigger and DomainObject#post_commit_trigger for
142
148
  # more.
143
149
  class ObjectStore < ContextualService::Service
144
- # Returns true if the current stored instance is a MockObjectStore.
145
- def self.mock?; get_object_store.mock?; end
146
-
147
150
  @@db_bridge = nil
151
+ @@db_type = 'Mysql'
148
152
 
149
153
  # Returns the DbBridge; this is useful in case you need to use raw SQL for
150
154
  # a specific query.
@@ -154,13 +158,20 @@ module Lafcadio
154
158
  DbConnection.db_name= dbName
155
159
  end
156
160
 
161
+ def self.db_type; @@db_type; end
162
+
163
+ def self.db_type=( dbt ); @@db_type = dbt; end
164
+
165
+ # Returns true if the current stored instance is a MockObjectStore.
166
+ def self.mock?; get_object_store.mock?; end
167
+
157
168
  def initialize #:nodoc:
158
169
  @cache = ObjectStore::Cache.new self.class.db_bridge
159
170
  end
160
171
 
161
172
  # Returns all domain objects for the given domain class.
162
- def all(domain_class)
163
- @cache.get_by_query( Query.new( domain_class ) )
173
+ def all( domain_class, opts = {} )
174
+ @cache.get_by_query( Query.new( domain_class, opts ) )
164
175
  end
165
176
 
166
177
  # Returns the DbBridge; this is useful in case you need to use raw SQL for
@@ -169,7 +180,8 @@ module Lafcadio
169
180
 
170
181
  # Returns the domain object corresponding to the domain class and pk_id.
171
182
  def get( domain_class, pk_id )
172
- @cache.get_by_query( Query.new( domain_class, pk_id ) ).first or (
183
+ qry = Query.new( domain_class, :pk_id => pk_id )
184
+ @cache.get_by_query( qry ).first or (
173
185
  raise(
174
186
  DomainObjectNotFoundError, "Can't find #{domain_class} #{pk_id}",
175
187
  caller
@@ -231,7 +243,7 @@ module Lafcadio
231
243
  def query(conditionOrQuery)
232
244
  if conditionOrQuery.class <= Query::Condition
233
245
  condition = conditionOrQuery
234
- query = Query.new condition.domain_class, condition
246
+ query = Query.new( condition.domain_class, :condition => condition )
235
247
  else
236
248
  query = conditionOrQuery
237
249
  end
@@ -256,9 +268,12 @@ module Lafcadio
256
268
  def transaction( &action ); @cache.transaction( action ); end
257
269
 
258
270
  class Cache #:nodoc:
271
+ include MonitorMixin
272
+
259
273
  attr_reader :db_bridge
260
274
 
261
275
  def initialize( db_bridge = DbBridge.new )
276
+ super()
262
277
  @db_bridge = db_bridge
263
278
  @domain_class_caches = {}
264
279
  end
@@ -268,8 +283,12 @@ module Lafcadio
268
283
  db_object.last_commit_type = get_last_commit db_object
269
284
  db_object.pre_commit_trigger
270
285
  update_dependent_domain_objects( db_object ) if db_object.delete
271
- @db_bridge.commit db_object
272
- db_object.pk_id = @db_bridge.last_pk_id_inserted unless db_object.pk_id
286
+ synchronize do
287
+ @db_bridge.commit db_object
288
+ unless db_object.pk_id
289
+ db_object.pk_id = @db_bridge.last_pk_id_inserted
290
+ end
291
+ end
273
292
  update_after_commit db_object
274
293
  db_object.post_commit_trigger
275
294
  db_object.reset_original_values_hash
@@ -285,20 +304,36 @@ module Lafcadio
285
304
  @domain_class_caches[domain_class]
286
305
  end
287
306
 
307
+ def get_by_query( query )
308
+ main_cache = cache query.domain_class
309
+ unless main_cache.queries[query]
310
+ if query.one_pk_id?
311
+ collected = false
312
+ else
313
+ collected = main_cache.collect_from_superset query
314
+ end
315
+ if !collected and main_cache.queries.values
316
+ newObjects = @db_bridge.select_dobjs query
317
+ newObjects.each { |dbObj| main_cache.save dbObj }
318
+ main_cache.queries[query] = newObjects.collect { |dobj| dobj.pk_id }
319
+ end
320
+ end
321
+ main_cache.queries[query].map { |pk_id| main_cache[pk_id] }.compact
322
+ end
323
+
288
324
  def get_last_commit( db_object )
289
325
  if db_object.delete
290
- DomainObject::COMMIT_DELETE
326
+ :delete
291
327
  elsif db_object.pk_id
292
- DomainObject::COMMIT_EDIT
328
+ :update
293
329
  else
294
- DomainObject::COMMIT_ADD
330
+ :insert
295
331
  end
296
332
  end
297
333
 
298
334
  def method_missing( meth, *args )
299
335
  simple_dispatch = [
300
- :get_by_query, :queries, :save, :set_commit_time,
301
- :update_after_commit
336
+ :queries, :save, :set_commit_time, :update_after_commit
302
337
  ]
303
338
  if simple_dispatch.include?( meth )
304
339
  cache( args.first.domain_class ).send( meth, *args )
@@ -379,29 +414,8 @@ module Lafcadio
379
414
  end
380
415
  end
381
416
 
382
- def get_by_query( query )
383
- unless queries[query]
384
- collected = collect_from_superset query
385
- if !collected and queries.values
386
- query_db query
387
- end
388
- end
389
- collection = []
390
- queries[query].each { |pk_id|
391
- dobj = self[ pk_id ]
392
- collection << dobj if dobj
393
- }
394
- collection
395
- end
396
-
397
417
  def last_commit_time( pk_id ); commit_times[pk_id]; end
398
418
 
399
- def query_db( query )
400
- newObjects = @db_bridge.select_dobjs query
401
- newObjects.each { |dbObj| save dbObj }
402
- queries[query] = newObjects.collect { |dobj| dobj.pk_id }
403
- end
404
-
405
419
  # Saves a domain object.
406
420
  def save(db_object)
407
421
  self[db_object.pk_id] = db_object
@@ -411,11 +425,11 @@ module Lafcadio
411
425
  def set_commit_time( d_obj ); commit_times[d_obj.pk_id] = Time.now; end
412
426
 
413
427
  def update_after_commit( db_object ) #:nodoc:
414
- if [ DomainObject::COMMIT_EDIT, DomainObject::COMMIT_ADD ].include?(
428
+ if [ :update, :insert ].include?(
415
429
  db_object.last_commit_type
416
430
  )
417
431
  save db_object
418
- elsif db_object.last_commit_type == DomainObject::COMMIT_DELETE
432
+ elsif db_object.last_commit_type == :delete
419
433
  flush db_object
420
434
  end
421
435
  set_commit_time db_object
@@ -430,7 +444,9 @@ module Lafcadio
430
444
  @obj = obj
431
445
  reversed = []
432
446
  @obj.class.self_and_concrete_superclasses.each { |domain_class|
433
- reversed << statement_bind_value_pair( domain_class )
447
+ statement_bind_value_pairs( domain_class ).reverse.each do |pair|
448
+ reversed << pair
449
+ end
434
450
  }
435
451
  reversed.reverse.each do |statement, binds|
436
452
  self << [ statement, binds ]
@@ -463,7 +479,8 @@ module Lafcadio
463
479
  fields = domain_class.class_fields
464
480
  nameValuePairs = get_name_value_pairs( domain_class )
465
481
  if domain_class.is_child_domain_class?
466
- nameValuePairs[domain_class.sql_primary_key_name] = 'LAST_INSERT_ID()'
482
+ pair = DbBridge.last_inserted_pk_id_pair domain_class.superclass
483
+ nameValuePairs[domain_class.sql_primary_key_name] = pair.first
467
484
  end
468
485
  fieldNameStr = nameValuePairs.keys.join ", "
469
486
  fieldValueStr = nameValuePairs.values.join ", "
@@ -471,18 +488,24 @@ module Lafcadio
471
488
  "values(#{fieldValueStr})"
472
489
  end
473
490
 
474
- def statement_bind_value_pair( domain_class )
491
+ def statement_bind_value_pairs( domain_class )
475
492
  @bind_values = []
476
493
  if @obj.pk_id == nil
477
494
  statement = insert_sql( domain_class )
495
+ pairs = [ [statement, @bind_values] ]
496
+ if domain_class.is_child_domain_class? and ObjectStore.db_type == 'Pg'
497
+ setval_sql = "select setval( '#{ domain_class.postgres_pk_id_seq }', currval('#{ domain_class.superclass.postgres_pk_id_seq }') )"
498
+ pairs << [ setval_sql, @bind_values ]
499
+ end
500
+ pairs
478
501
  else
479
502
  if @obj.delete
480
503
  statement = delete_sql( domain_class )
481
504
  else
482
505
  statement = update_sql( domain_class)
483
506
  end
507
+ [ [statement, @bind_values] ]
484
508
  end
485
- [statement, @bind_values]
486
509
  end
487
510
 
488
511
  def update_sql( domain_class )
@@ -512,8 +535,18 @@ module Lafcadio
512
535
  new
513
536
  end
514
537
 
538
+ def self.last_inserted_pk_id_pair( domain_class )
539
+ case ObjectStore.db_type
540
+ when 'Mysql'
541
+ [ 'last_insert_id()', 'last_insert_id()' ]
542
+ when 'Pg'
543
+ [ "currval('#{ domain_class.postgres_pk_id_seq }')", 'currval' ]
544
+ end
545
+ end
546
+
515
547
  def initialize
516
548
  @db_conn = DbConnection.get_db_connection
549
+ @transaction = nil
517
550
  ObjectSpace.define_finalizer( self, proc { |id|
518
551
  DbConnection.get_db_connection.disconnect
519
552
  } )
@@ -532,9 +565,24 @@ module Lafcadio
532
565
  @db_conn.do( sql, *binds )
533
566
  end
534
567
  if statements_and_binds[0].first =~ /insert/
535
- sql = 'select last_insert_id()'
536
- result = select_all( sql )
537
- @@last_pk_id_inserted = result[0]['last_insert_id()'].to_i
568
+ @@last_pk_id_inserted = get_last_pk_id_inserted(
569
+ db_object.domain_class
570
+ )
571
+ end
572
+ @db_conn.do( 'commit' ) unless @transaction
573
+ end
574
+
575
+ def get_last_pk_id_inserted( domain_class )
576
+ pair = self.class.last_inserted_pk_id_pair( domain_class )
577
+ sql = 'select ' + pair.first
578
+ begin
579
+ select_all( sql ).first[pair.last].to_i
580
+ rescue RuntimeError
581
+ error_msg =
582
+ "The field \"" + domain_class.sql_primary_key_name +
583
+ "\" can\'t be found in the table \"" +
584
+ domain_class.table_name + "\"."
585
+ raise FieldMatchError, error_msg, caller
538
586
  end
539
587
  end
540
588
 
@@ -570,24 +618,39 @@ module Lafcadio
570
618
 
571
619
  def select_dobjs(query)
572
620
  domain_class = query.domain_class
573
- select_all( query.to_sql ).collect { |row_hash|
574
- domain_class.new( SqlToRubyValues.new( domain_class, row_hash ) )
621
+ select_all( query.to_sql( ObjectStore.db_type ) ).collect { |row_hash|
622
+ dobj = domain_class.new(
623
+ SqlToRubyValues.new( domain_class, row_hash )
624
+ )
625
+ if query.include
626
+ query.include.each do |include_sym|
627
+ field = domain_class.field include_sym
628
+ included_dclass = field.linked_type
629
+ if dobj.send( field.name )
630
+ dobj.send( field.name ).db_object = included_dclass.new(
631
+ SqlToRubyValues.new( included_dclass, row_hash )
632
+ )
633
+ end
634
+ end
635
+ end
636
+ dobj
575
637
  }
576
638
  end
577
639
 
578
640
  def transaction( action )
579
- tr = Transaction.new @db_conn
580
- tr.commit
641
+ @transaction = Transaction.new @db_conn
642
+ @transaction.commit
581
643
  begin
582
- action.call tr
583
- tr.commit
644
+ action.call @transaction
645
+ @transaction.commit
584
646
  rescue RollbackError
585
647
  # rollback handled by Transaction
586
648
  rescue
587
649
  err_to_raise = $!
588
- tr.rollback false
650
+ @transaction.rollback false
589
651
  raise err_to_raise
590
652
  end
653
+ @transaction = nil
591
654
  end
592
655
 
593
656
  class Transaction #:nodoc:
@@ -617,17 +680,21 @@ module Lafcadio
617
680
 
618
681
  def disconnect; @dbh.disconnect; end
619
682
 
620
- def load_new_dbh
683
+ def driver_url
621
684
  config = LafcadioConfig.new
622
685
  dbName = @@db_name || config['dbname']
623
- dbAndHost = nil
686
+ driver_name = config['dbtype'] || 'Mysql'
624
687
  if dbName && config['dbhost']
625
- dbAndHost = "dbi:Mysql:#{ dbName }:#{ config['dbhost'] }"
688
+ "dbi:#{ driver_name }:#{ dbName }:#{ config['dbhost'] }"
626
689
  else
627
- dbAndHost = "dbi:#{config['dbconn']}"
690
+ "dbi:#{config['dbconn']}"
628
691
  end
692
+ end
693
+
694
+ def load_new_dbh
695
+ config = LafcadioConfig.new
629
696
  dbh = @@conn_class.connect(
630
- dbAndHost, config['dbuser'], config['dbpassword']
697
+ driver_url, config['dbuser'], config['dbpassword']
631
698
  )
632
699
  dbh['AutoCommit'] = false
633
700
  dbh
@@ -3,6 +3,7 @@ require 'lafcadio/depend'
3
3
  require 'lafcadio/domain'
4
4
  require 'lafcadio/query'
5
5
  require 'lafcadio/util'
6
+ require 'monitor'
6
7
 
7
8
  module Lafcadio
8
9
  class DomainObjectInitError < RuntimeError #:nodoc:
@@ -12,11 +13,11 @@ module Lafcadio
12
13
  end
13
14
 
14
15
  # The DomainObjectProxy is used when retrieving domain objects that are
15
- # linked to other domain objects with DomainObjectFields. In terms of +domain_class+
16
- # and
17
- # +pk_id+, a DomainObjectProxy instance looks to the outside world like the
18
- # domain object it's supposed to represent. It only retrieves its domain
19
- # object from the database when member data is requested.
16
+ # linked to other domain objects with DomainObjectFields. In terms of
17
+ # +domain_class+ and +pk_id+, a DomainObjectProxy instance looks to the
18
+ # outside world like the domain object it's supposed to represent. It only
19
+ # retrieves its domain object from the database when member data is
20
+ # requested.
20
21
  #
21
22
  # In normal usage you will probably never manipulate a DomainObjectProxy
22
23
  # directly, but you may discover it by accident by calling
@@ -27,7 +28,7 @@ module Lafcadio
27
28
 
28
29
  attr_accessor :domain_class, :pk_id
29
30
 
30
- def initialize( *args )
31
+ def initialize( *args ) #:nodoc:
31
32
  if args.size == 2
32
33
  @domain_class = args.first
33
34
  @pk_id = args.last
@@ -40,29 +41,34 @@ module Lafcadio
40
41
  end
41
42
  end
42
43
 
43
- def db_object
44
+ def db_object #:nodoc:
44
45
  if @db_object.nil? || needs_refresh?
45
- @db_object = ObjectStore.get_object_store.get( @domain_class, @pk_id )
46
- @d_obj_retrieve_time = Time.now
46
+ dbo = ObjectStore.get_object_store.get( @domain_class, @pk_id )
47
+ self.db_object = dbo
47
48
  end
48
49
  @db_object
49
50
  end
51
+
52
+ def db_object=( dbo )
53
+ @db_object = dbo
54
+ @d_obj_retrieve_time = Time.now
55
+ end
50
56
 
51
- def hash
57
+ def hash #:nodoc:
52
58
  db_object.hash
53
59
  end
54
60
 
55
- def method_missing( methodId, *args )
61
+ def method_missing( methodId, *args ) #:nodoc:
56
62
  db_object.send( methodId, *args )
57
63
  end
58
64
 
59
- def needs_refresh?
65
+ def needs_refresh? #:nodoc:
60
66
  object_store = ObjectStore.get_object_store
61
67
  last_commit_time = object_store.last_commit_time( @domain_class, @pk_id )
62
68
  last_commit_time && last_commit_time > @d_obj_retrieve_time
63
69
  end
64
70
 
65
- def to_s
71
+ def to_s #:nodoc:
66
72
  db_object.to_s
67
73
  end
68
74
  end
@@ -80,29 +86,31 @@ module Lafcadio
80
86
  # [dbhost] The database host.
81
87
  #
82
88
  # = Instantiating ObjectStore
83
- # The ObjectStore is a ContextualService, meaning you can't get an instance by
84
- # calling ObjectStore.new. Instead, you should call
85
- # ObjectStore.get_object_store. (Using a ContextualService makes it easier to
86
- # make out the ObjectStore for unit tests: See ContextualService for more.)
89
+ # You can't get an instance of ObjectStore by calling ObjectStore.new.
90
+ # Instead, you should call ObjectStore.get_object_store.
87
91
  #
88
92
  # = Dynamic method calls
89
93
  # ObjectStore uses reflection to provide a lot of convenience methods for
90
94
  # querying domain objects in a number of ways.
91
- # [ObjectStore#get< domain class > (pk_id)]
95
+ # [ObjectStore#< domain class >( pk_id )]
92
96
  # Retrieves one domain object by pk_id. For example,
93
- # ObjectStore#getUser( 100 )
94
- # will return User 100.
95
- # [ObjectStore#get< domain class >s (searchTerm, fieldName = nil)]
97
+ # ObjectStore#user( 100 )
98
+ # will return User 100. Note that you can also just user DomainObject.[]:
99
+ # User[100]
100
+ # [ObjectStore#< plural of domain class >( searchTerm, fieldName = nil )]
96
101
  # Returns a collection of all instances of that domain class matching that
97
102
  # search term. For example,
98
- # ObjectStore#getProducts( aProductCategory )
103
+ # ObjectStore#products( a_product_category )
99
104
  # queries MySQL for all products that belong to that product category. You
100
105
  # can omit +fieldName+ if +searchTerm+ is a non-nil domain object, and the
101
106
  # field connecting the first domain class to the second is named after the
102
107
  # domain class. (For example, the above line assumes that Product has a
103
- # field named "productCategory".) Otherwise, it's best to include
108
+ # field named "product_category".) Otherwise, it's best to include
104
109
  # +fieldName+:
105
- # ObjectStore#getUsers( "Jones", "lastName" )
110
+ # ObjectStore#users( "Jones", "lastName" )
111
+ # Note that these can also be accessed through DomainObject.get:
112
+ # Product.get( a_product_category )
113
+ # User.get( "Jones", "lastName" )
106
114
  #
107
115
  # = Querying
108
116
  # ObjectStore can also be used to generate complex, ad-hoc queries which
@@ -110,14 +118,19 @@ module Lafcadio
110
118
  # Furthermore, these queries can be run against in-memory data stores, which
111
119
  # is particularly useful for tests.
112
120
  # date = Date.new( 2003, 1, 1 )
113
- # ObjectStore#getInvoices { |invoice|
114
- # Query.And( invoice.date.gte( date ), invoice.rate.equals( 10 ),
115
- # invoice.hours.equals( 10 ) )
121
+ # ObjectStore#invoices { |invoice|
122
+ # invoice.date.gte( date ) & invoice.rate.equals( 10 ) &
123
+ # invoice.hours.equals( 10 )
116
124
  # }
117
125
  # is the same as
118
126
  # select * from invoices
119
127
  # where (date >= '2003-01-01' and rate = 10 and hours = 10)
120
- # See lafcadio/query.rb for more.
128
+ # Note that you can also use DomainObject.get:
129
+ # Invoice.get { |invoice|
130
+ # invoice.date.gte( date ) & invoice.rate.equals( 10 ) &
131
+ # invoice.hours.equals( 10 )
132
+ # }
133
+ # See lafcadio/query.rb for more on the query inference syntax.
121
134
  #
122
135
  # = SQL Logging
123
136
  # Lafcadio uses log4r to log all of its SQL statements. The simplest way to
@@ -131,25 +144,44 @@ module Lafcadio
131
144
  # = Triggers
132
145
  # Domain classes can be set to fire triggers either before or after commits.
133
146
  # Since these triggers are executed in Ruby, they're easy to test. See
134
- # DomainObject#pre_commit_trigger and DomainObject#post_commit_trigger for more.
147
+ # DomainObject#pre_commit_trigger and DomainObject#post_commit_trigger for
148
+ # more.
135
149
  class ObjectStore < ContextualService::Service
136
- def self.mock?; get_object_store.mock?; end
137
-
138
150
  @@db_bridge = nil
139
-
151
+ @@db_type = 'Mysql'
152
+
153
+ # Returns the DbBridge; this is useful in case you need to use raw SQL for
154
+ # a specific query.
140
155
  def self.db_bridge; @@db_bridge ||= DbBridge.new; end
141
156
 
142
- def self.db_name= (dbName) #:nodoc:
157
+ def self.db_name= (dbName)
143
158
  DbConnection.db_name= dbName
144
159
  end
145
160
 
161
+ def self.db_type; @@db_type; end
162
+
163
+ def self.db_type=( dbt ); @@db_type = dbt; end
164
+
165
+ # Returns true if the current stored instance is a MockObjectStore.
166
+ def self.mock?; get_object_store.mock?; end
167
+
146
168
  def initialize #:nodoc:
147
169
  @cache = ObjectStore::Cache.new self.class.db_bridge
148
170
  end
149
171
 
172
+ # Returns all domain objects for the given domain class.
173
+ def all( domain_class, opts = {} )
174
+ @cache.get_by_query( Query.new( domain_class, opts ) )
175
+ end
176
+
177
+ # Returns the DbBridge; this is useful in case you need to use raw SQL for
178
+ # a specific query.
179
+ def db_bridge; @cache.db_bridge; end
180
+
150
181
  # Returns the domain object corresponding to the domain class and pk_id.
151
182
  def get( domain_class, pk_id )
152
- @cache.get_by_query( Query.new( domain_class, pk_id ) ).first or (
183
+ qry = Query.new( domain_class, :pk_id => pk_id )
184
+ @cache.get_by_query( qry ).first or (
153
185
  raise(
154
186
  DomainObjectNotFoundError, "Can't find #{domain_class} #{pk_id}",
155
187
  caller
@@ -157,23 +189,6 @@ module Lafcadio
157
189
  )
158
190
  end
159
191
 
160
- # Returns all domain objects for the given domain class.
161
- def get_all(domain_class)
162
- @cache.get_by_query( Query.new( domain_class ) )
163
- end
164
-
165
- # Returns the DbBridge; this is useful in case you need to use raw SQL for a
166
- # specific query.
167
- def get_db_bridge; @cache.db_bridge; end
168
-
169
- def get_filtered(domain_class_name, searchTerm, fieldName = nil) #:nodoc:
170
- domain_class = Class.by_name domain_class_name
171
- unless fieldName
172
- fieldName = domain_class.link_field( searchTerm.domain_class ).name
173
- end
174
- get_subset( Query::Equals.new( fieldName, searchTerm, domain_class ) )
175
- end
176
-
177
192
  def get_map_object( domain_class, map1, map2 ) #:nodoc:
178
193
  unless map1 && map2
179
194
  raise ArgumentError,
@@ -187,44 +202,23 @@ module Lafcadio
187
202
  map2.domain_class.basename.camel_case_to_underscore
188
203
  ).equals( map2 )
189
204
  }
190
- get_subset( query ).first
205
+ query( query ).first
206
+ end
207
+
208
+ def group_query( query ) #:nodoc:
209
+ @cache.group_query( query )
191
210
  end
192
211
 
193
212
  # Retrieves the maximum value across all instances of one domain class.
194
- # ObjectStore#get_max( Client )
213
+ # ObjectStore#max( Client )
195
214
  # returns the highest +pk_id+ in the +clients+ table.
196
- # ObjectStore#get_max( Invoice, "rate" )
215
+ # ObjectStore#max( Invoice, "rate" )
197
216
  # will return the highest rate for all invoices.
198
- def get_max( domain_class, field_name = 'pk_id' )
217
+ def max( domain_class, field_name = 'pk_id' )
199
218
  qry = Query::Max.new( domain_class, field_name )
200
219
  @cache.group_query( qry ).only[:max]
201
220
  end
202
221
 
203
- # Retrieves a collection of domain objects by +pk_id+.
204
- # ObjectStore#get_objects( Clients, [ 1, 2, 3 ] )
205
- def get_objects( domain_class, pk_ids )
206
- if pk_ids.is_a?( Array ) && pk_ids.all? { |elt| elt.is_a?( Integer ) }
207
- get_subset Query::In.new( 'pk_id', pk_ids, domain_class )
208
- else
209
- raise(
210
- ArgumentError,
211
- "ObjectStore#get_objects( domain_class, pk_ids ): pk_ids needs to " +
212
- "be an array of integers",
213
- caller
214
- )
215
- end
216
- end
217
-
218
- def get_subset(conditionOrQuery) #:nodoc:
219
- if conditionOrQuery.class <= Query::Condition
220
- condition = conditionOrQuery
221
- query = Query.new condition.domain_class, condition
222
- else
223
- query = conditionOrQuery
224
- end
225
- @cache.get_by_query( query )
226
- end
227
-
228
222
  def method_missing(methodId, *args) #:nodoc:
229
223
  if [ :commit, :flush, :last_commit_time ].include?( methodId )
230
224
  @cache.send( methodId, *args )
@@ -232,7 +226,7 @@ module Lafcadio
232
226
  proc = block_given? ? ( proc { |obj| yield( obj ) } ) : nil
233
227
  dispatch = MethodDispatch.new( methodId, proc, *args )
234
228
  if dispatch.symbol
235
- self.send( dispatch.symbol, *dispatch.args )
229
+ dispatch.dispatch self
236
230
  else
237
231
  super
238
232
  end
@@ -243,9 +237,20 @@ module Lafcadio
243
237
  false
244
238
  end
245
239
 
246
- def query( query ); @cache.group_query( query ); end
247
-
248
- def respond_to?( symbol, include_private = false )
240
+ # Passes a query and selects with it.
241
+ # qry = Query.infer( User ) { |user| user.fname.equals( 'Francis' ) }
242
+ # francises = ObjectStore.get_object_store.query( qry )
243
+ def query(conditionOrQuery)
244
+ if conditionOrQuery.class <= Query::Condition
245
+ condition = conditionOrQuery
246
+ query = Query.new( condition.domain_class, :condition => condition )
247
+ else
248
+ query = conditionOrQuery
249
+ end
250
+ @cache.get_by_query( query )
251
+ end
252
+
253
+ def respond_to?( symbol, include_private = false ) #:nodoc:
249
254
  if MethodDispatch.new( symbol ).symbol
250
255
  true
251
256
  else
@@ -253,12 +258,22 @@ module Lafcadio
253
258
  end
254
259
  end
255
260
 
261
+ # As long as the underlying database table sorts transactions, you can use
262
+ # this to run transactional logic. These transactions will auto commit at
263
+ # the end of the block, and can be rolled back.
264
+ # ObjectStore.get_object_store.transaction do |tr|
265
+ # Client.new( 'name' => 'Big Co.' ).commit
266
+ # tr.rollback
267
+ # end # the client will not be saved to the DB
256
268
  def transaction( &action ); @cache.transaction( action ); end
257
269
 
258
270
  class Cache #:nodoc:
271
+ include MonitorMixin
272
+
259
273
  attr_reader :db_bridge
260
274
 
261
275
  def initialize( db_bridge = DbBridge.new )
276
+ super()
262
277
  @db_bridge = db_bridge
263
278
  @domain_class_caches = {}
264
279
  end
@@ -268,10 +283,15 @@ module Lafcadio
268
283
  db_object.last_commit_type = get_last_commit db_object
269
284
  db_object.pre_commit_trigger
270
285
  update_dependent_domain_objects( db_object ) if db_object.delete
271
- @db_bridge.commit db_object
272
- db_object.pk_id = @db_bridge.last_pk_id_inserted unless db_object.pk_id
286
+ synchronize do
287
+ @db_bridge.commit db_object
288
+ unless db_object.pk_id
289
+ db_object.pk_id = @db_bridge.last_pk_id_inserted
290
+ end
291
+ end
273
292
  update_after_commit db_object
274
293
  db_object.post_commit_trigger
294
+ db_object.reset_original_values_hash
275
295
  db_object
276
296
  end
277
297
 
@@ -284,20 +304,36 @@ module Lafcadio
284
304
  @domain_class_caches[domain_class]
285
305
  end
286
306
 
307
+ def get_by_query( query )
308
+ main_cache = cache query.domain_class
309
+ unless main_cache.queries[query]
310
+ if query.one_pk_id?
311
+ collected = false
312
+ else
313
+ collected = main_cache.collect_from_superset query
314
+ end
315
+ if !collected and main_cache.queries.values
316
+ newObjects = @db_bridge.select_dobjs query
317
+ newObjects.each { |dbObj| main_cache.save dbObj }
318
+ main_cache.queries[query] = newObjects.collect { |dobj| dobj.pk_id }
319
+ end
320
+ end
321
+ main_cache.queries[query].map { |pk_id| main_cache[pk_id] }.compact
322
+ end
323
+
287
324
  def get_last_commit( db_object )
288
325
  if db_object.delete
289
- DomainObject::COMMIT_DELETE
326
+ :delete
290
327
  elsif db_object.pk_id
291
- DomainObject::COMMIT_EDIT
328
+ :update
292
329
  else
293
- DomainObject::COMMIT_ADD
330
+ :insert
294
331
  end
295
332
  end
296
333
 
297
334
  def method_missing( meth, *args )
298
335
  simple_dispatch = [
299
- :get_by_query, :queries, :save, :set_commit_time,
300
- :update_after_commit
336
+ :queries, :save, :set_commit_time, :update_after_commit
301
337
  ]
302
338
  if simple_dispatch.include?( meth )
303
339
  cache( args.first.domain_class ).send( meth, *args )
@@ -310,9 +346,7 @@ module Lafcadio
310
346
 
311
347
  def update_dependent_domain_class( db_object, aClass, field )
312
348
  object_store = ObjectStore.get_object_store
313
- collection = object_store.get_filtered(
314
- aClass.name, db_object, field.name
315
- )
349
+ collection = aClass.get( db_object, field.name )
316
350
  collection.each { |dependentObject|
317
351
  if field.delete_cascade
318
352
  dependentObject.delete = true
@@ -332,7 +366,7 @@ module Lafcadio
332
366
  }
333
367
  end
334
368
 
335
- class DomainClassCache < Hash
369
+ class DomainClassCache < Hash #:nodoc:
336
370
  attr_reader :commit_times, :domain_class, :queries
337
371
 
338
372
  def initialize( domain_class, db_bridge )
@@ -349,11 +383,11 @@ module Lafcadio
349
383
 
350
384
  def collect_from_superset( query )
351
385
  if ( pk_ids = find_superset_pk_ids( query ) )
352
- queries[query] = ( pk_ids.collect { |pk_id|
386
+ db_objects = ( pk_ids.collect { |pk_id|
353
387
  self[ pk_id ]
354
- } ).select { |dobj| query.object_meets( dobj ) }.collect { |dobj|
355
- dobj.pk_id
356
- }
388
+ } ).select { |dobj| query.dobj_satisfies?( dobj ) }
389
+ db_objects = query.order_and_limit_collection db_objects
390
+ queries[query] = db_objects.collect { |dobj| dobj.pk_id }
357
391
  true
358
392
  else
359
393
  false
@@ -380,29 +414,8 @@ module Lafcadio
380
414
  end
381
415
  end
382
416
 
383
- def get_by_query( query )
384
- unless queries[query]
385
- collected = collect_from_superset query
386
- if !collected and queries.values
387
- query_db query
388
- end
389
- end
390
- collection = []
391
- queries[query].each { |pk_id|
392
- dobj = self[ pk_id ]
393
- collection << dobj if dobj
394
- }
395
- collection
396
- end
397
-
398
417
  def last_commit_time( pk_id ); commit_times[pk_id]; end
399
418
 
400
- def query_db( query )
401
- newObjects = @db_bridge.select_dobjs query
402
- newObjects.each { |dbObj| save dbObj }
403
- queries[query] = newObjects.collect { |dobj| dobj.pk_id }
404
- end
405
-
406
419
  # Saves a domain object.
407
420
  def save(db_object)
408
421
  self[db_object.pk_id] = db_object
@@ -412,11 +425,11 @@ module Lafcadio
412
425
  def set_commit_time( d_obj ); commit_times[d_obj.pk_id] = Time.now; end
413
426
 
414
427
  def update_after_commit( db_object ) #:nodoc:
415
- if [ DomainObject::COMMIT_EDIT, DomainObject::COMMIT_ADD ].include?(
428
+ if [ :update, :insert ].include?(
416
429
  db_object.last_commit_type
417
430
  )
418
431
  save db_object
419
- elsif db_object.last_commit_type == DomainObject::COMMIT_DELETE
432
+ elsif db_object.last_commit_type == :delete
420
433
  flush db_object
421
434
  end
422
435
  set_commit_time db_object
@@ -431,7 +444,9 @@ module Lafcadio
431
444
  @obj = obj
432
445
  reversed = []
433
446
  @obj.class.self_and_concrete_superclasses.each { |domain_class|
434
- reversed << statement_bind_value_pair( domain_class )
447
+ statement_bind_value_pairs( domain_class ).reverse.each do |pair|
448
+ reversed << pair
449
+ end
435
450
  }
436
451
  reversed.reverse.each do |statement, binds|
437
452
  self << [ statement, binds ]
@@ -464,7 +479,8 @@ module Lafcadio
464
479
  fields = domain_class.class_fields
465
480
  nameValuePairs = get_name_value_pairs( domain_class )
466
481
  if domain_class.is_child_domain_class?
467
- nameValuePairs[domain_class.sql_primary_key_name] = 'LAST_INSERT_ID()'
482
+ pair = DbBridge.last_inserted_pk_id_pair domain_class.superclass
483
+ nameValuePairs[domain_class.sql_primary_key_name] = pair.first
468
484
  end
469
485
  fieldNameStr = nameValuePairs.keys.join ", "
470
486
  fieldValueStr = nameValuePairs.values.join ", "
@@ -472,18 +488,24 @@ module Lafcadio
472
488
  "values(#{fieldValueStr})"
473
489
  end
474
490
 
475
- def statement_bind_value_pair( domain_class )
491
+ def statement_bind_value_pairs( domain_class )
476
492
  @bind_values = []
477
493
  if @obj.pk_id == nil
478
494
  statement = insert_sql( domain_class )
495
+ pairs = [ [statement, @bind_values] ]
496
+ if domain_class.is_child_domain_class? and ObjectStore.db_type == 'Pg'
497
+ setval_sql = "select setval( '#{ domain_class.postgres_pk_id_seq }', currval('#{ domain_class.superclass.postgres_pk_id_seq }') )"
498
+ pairs << [ setval_sql, @bind_values ]
499
+ end
500
+ pairs
479
501
  else
480
502
  if @obj.delete
481
503
  statement = delete_sql( domain_class )
482
504
  else
483
505
  statement = update_sql( domain_class)
484
506
  end
507
+ [ [statement, @bind_values] ]
485
508
  end
486
- [statement, @bind_values]
487
509
  end
488
510
 
489
511
  def update_sql( domain_class )
@@ -513,8 +535,18 @@ module Lafcadio
513
535
  new
514
536
  end
515
537
 
538
+ def self.last_inserted_pk_id_pair( domain_class )
539
+ case ObjectStore.db_type
540
+ when 'Mysql'
541
+ [ 'last_insert_id()', 'last_insert_id()' ]
542
+ when 'Pg'
543
+ [ "currval('#{ domain_class.postgres_pk_id_seq }')", 'currval' ]
544
+ end
545
+ end
546
+
516
547
  def initialize
517
548
  @db_conn = DbConnection.get_db_connection
549
+ @transaction = nil
518
550
  ObjectSpace.define_finalizer( self, proc { |id|
519
551
  DbConnection.get_db_connection.disconnect
520
552
  } )
@@ -533,10 +565,17 @@ module Lafcadio
533
565
  @db_conn.do( sql, *binds )
534
566
  end
535
567
  if statements_and_binds[0].first =~ /insert/
536
- sql = 'select last_insert_id()'
537
- result = select_all( sql )
538
- @@last_pk_id_inserted = result[0]['last_insert_id()'].to_i
568
+ @@last_pk_id_inserted = get_last_pk_id_inserted(
569
+ db_object.domain_class
570
+ )
539
571
  end
572
+ @db_conn.do( 'commit' ) unless @transaction
573
+ end
574
+
575
+ def get_last_pk_id_inserted( domain_class )
576
+ pair = self.class.last_inserted_pk_id_pair( domain_class )
577
+ sql = 'select ' + pair.first
578
+ select_all( sql ).first[pair.last].to_i
540
579
  end
541
580
 
542
581
  def group_query( query )
@@ -572,26 +611,41 @@ module Lafcadio
572
611
  def select_dobjs(query)
573
612
  domain_class = query.domain_class
574
613
  select_all( query.to_sql ).collect { |row_hash|
575
- domain_class.new( SqlToRubyValues.new( domain_class, row_hash ) )
614
+ dobj = domain_class.new(
615
+ SqlToRubyValues.new( domain_class, row_hash )
616
+ )
617
+ if query.include
618
+ query.include.each do |include_sym|
619
+ field = domain_class.field include_sym
620
+ included_dclass = field.linked_type
621
+ if dobj.send( field.name )
622
+ dobj.send( field.name ).db_object = included_dclass.new(
623
+ SqlToRubyValues.new( included_dclass, row_hash )
624
+ )
625
+ end
626
+ end
627
+ end
628
+ dobj
576
629
  }
577
630
  end
578
631
 
579
632
  def transaction( action )
580
- tr = Transaction.new @db_conn
581
- tr.commit
633
+ @transaction = Transaction.new @db_conn
634
+ @transaction.commit
582
635
  begin
583
- action.call tr
584
- tr.commit
636
+ action.call @transaction
637
+ @transaction.commit
585
638
  rescue RollbackError
586
639
  # rollback handled by Transaction
587
640
  rescue
588
641
  err_to_raise = $!
589
- tr.rollback false
642
+ @transaction.rollback false
590
643
  raise err_to_raise
591
644
  end
645
+ @transaction = nil
592
646
  end
593
647
 
594
- class Transaction
648
+ class Transaction #:nodoc:
595
649
  def initialize( db_conn ); @db_conn = db_conn; end
596
650
 
597
651
  def commit; @db_conn.commit; end
@@ -602,10 +656,11 @@ module Lafcadio
602
656
  end
603
657
  end
604
658
 
605
- class RollbackError < StandardError; end
659
+ class RollbackError < StandardError #:nodoc:
660
+ end
606
661
  end
607
662
 
608
- class DbConnection < ContextualService::Service
663
+ class DbConnection < ContextualService::Service #:nodoc:
609
664
  @@conn_class = DBI
610
665
  @@db_name = nil
611
666
 
@@ -617,17 +672,21 @@ module Lafcadio
617
672
 
618
673
  def disconnect; @dbh.disconnect; end
619
674
 
620
- def load_new_dbh
675
+ def driver_url
621
676
  config = LafcadioConfig.new
622
677
  dbName = @@db_name || config['dbname']
623
- dbAndHost = nil
678
+ driver_name = config['dbtype'] || 'Mysql'
624
679
  if dbName && config['dbhost']
625
- dbAndHost = "dbi:Mysql:#{ dbName }:#{ config['dbhost'] }"
680
+ "dbi:#{ driver_name }:#{ dbName }:#{ config['dbhost'] }"
626
681
  else
627
- dbAndHost = "dbi:#{config['dbconn']}"
682
+ "dbi:#{config['dbconn']}"
628
683
  end
684
+ end
685
+
686
+ def load_new_dbh
687
+ config = LafcadioConfig.new
629
688
  dbh = @@conn_class.connect(
630
- dbAndHost, config['dbuser'], config['dbpassword']
689
+ driver_url, config['dbuser'], config['dbpassword']
631
690
  )
632
691
  dbh['AutoCommit'] = false
633
692
  dbh
@@ -646,69 +705,75 @@ module Lafcadio
646
705
  @orig_args = other_args
647
706
  @maybe_proc = @orig_args.shift if @orig_args.size > 0
648
707
  @methodName = orig_method.id2name
649
- dispatch_get_method if @methodName =~ /^get(.*)$/
708
+ dispatch_method
709
+ end
710
+
711
+ def camel_case_method_name
712
+ @orig_method.id2name.underscore_to_camel_case
650
713
  end
651
714
 
652
- def camel_case_method_name_after_get
653
- @orig_method.id2name =~ /^get(.*)$/
654
- $1.underscore_to_camel_case
715
+ def dispatch( object_store )
716
+ target = ( @target or object_store )
717
+ target.send( @symbol, *@args )
655
718
  end
656
719
 
657
- def dispatch_get_all
658
- @symbol = :get_all
720
+ def dispatch_all
721
+ @symbol = :all
659
722
  @args = [ @domain_class ]
660
723
  end
661
724
 
662
- def dispatch_get_filtered( searchTerm, fieldName )
663
- @symbol = :get_filtered
664
- @args = [ @domain_class.name, searchTerm, fieldName ]
725
+ def dispatch_domain_class_get( searchTerm, fieldName )
726
+ @symbol = :get
727
+ @args = [ searchTerm, fieldName ]
728
+ @target = @domain_class
665
729
  end
666
-
730
+
667
731
  def dispatch_get_map_object( domain_class )
668
732
  @symbol = :get_map_object
669
733
  @args = [ domain_class, @orig_args[0], @orig_args[1] ]
670
734
  end
671
735
 
672
- def dispatch_get_method
673
- unless ( dispatch_get_singular )
674
- domain_class_name = camel_case_method_name_after_get.singular
675
- begin
676
- @domain_class = Module.by_name domain_class_name
677
- dispatch_get_plural
678
- rescue NameError
679
- # skip it
680
- end
681
- end
682
- end
683
-
684
- def dispatch_get_plural
736
+ def dispatch_plural
685
737
  if @orig_args.size == 0 && @maybe_proc.nil?
686
- dispatch_get_all
738
+ dispatch_all
687
739
  else
688
740
  searchTerm, fieldName = @orig_args[0..1]
689
741
  if searchTerm.nil? && @maybe_proc.nil? && fieldName.nil?
690
- raise_get_plural_needs_field_arg_if_first_arg_nil
742
+ raise_plural_needs_field_arg_if_first_arg_nil
691
743
  elsif !@maybe_proc.nil? && searchTerm.nil?
692
- dispatch_get_plural_by_query_block
744
+ dispatch_plural_by_query_block
693
745
  elsif @maybe_proc.nil? && ( !( searchTerm.nil? && fieldName.nil? ) )
694
- dispatch_get_filtered( searchTerm, fieldName )
746
+ dispatch_domain_class_get( searchTerm, fieldName )
695
747
  else
696
- raise_get_plural_cant_have_both_query_block_and_search_term
748
+ raise_plural_cant_have_both_query_block_and_search_term
697
749
  end
698
750
  end
699
751
  end
700
752
 
701
- def dispatch_get_plural_by_query_block
753
+ def dispatch_plural_by_query_block
702
754
  inferrer = Query::Inferrer.new( @domain_class ) { |obj|
703
755
  @maybe_proc.call( obj )
704
756
  }
705
- @symbol = :get_subset
757
+ @symbol = :query
706
758
  @args = [ inferrer.execute ]
707
759
  end
708
760
 
709
- def dispatch_get_singular
761
+ def dispatch_method
762
+ unless ( dispatch_singular )
763
+ domain_class_name = camel_case_method_name.singular
764
+ begin
765
+ @domain_class = Module.by_name domain_class_name
766
+ dispatch_plural
767
+ rescue NameError
768
+ # skip it
769
+ end
770
+ end
771
+ end
772
+
773
+ def dispatch_singular
710
774
  begin
711
- domain_class = Module.by_name camel_case_method_name_after_get
775
+ d_class_name = @orig_method.id2name.underscore_to_camel_case
776
+ domain_class = Module.by_name d_class_name
712
777
  if @orig_args[0].class <= Integer
713
778
  @symbol = :get
714
779
  @args = [ domain_class, @orig_args[0] ]
@@ -723,13 +788,13 @@ module Lafcadio
723
788
  end
724
789
  end
725
790
 
726
- def raise_get_plural_needs_field_arg_if_first_arg_nil
791
+ def raise_plural_needs_field_arg_if_first_arg_nil
727
792
  msg = "ObjectStore\##{ @orig_method } needs a field name as its " +
728
793
  "second argument if its first argument is nil"
729
794
  raise( ArgumentError, msg, caller )
730
795
  end
731
796
 
732
- def raise_get_plural_cant_have_both_query_block_and_search_term
797
+ def raise_plural_cant_have_both_query_block_and_search_term
733
798
  raise(
734
799
  ArgumentError, "Shouldn't send both a query block and a search term",
735
800
  caller