lafcadio 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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