lafcadio 0.8.3 → 0.9.0
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.
- data/lib/lafcadio.rb +1 -1
- data/lib/lafcadio.rb~ +1 -1
- data/lib/lafcadio/dateTime.rb~ +93 -0
- data/lib/lafcadio/depend.rb~ +8 -0
- data/lib/lafcadio/domain.rb +127 -120
- data/lib/lafcadio/domain.rb~ +48 -40
- data/lib/lafcadio/mock.rb +9 -12
- data/lib/lafcadio/mock.rb~ +110 -0
- data/lib/lafcadio/objectField.rb +6 -6
- data/lib/lafcadio/objectField.rb~ +564 -0
- data/lib/lafcadio/objectStore.rb +45 -24
- data/lib/lafcadio/objectStore.rb.~1.64.~ +766 -0
- data/lib/lafcadio/objectStore.rb~ +57 -26
- data/lib/lafcadio/query.rb +57 -22
- data/lib/lafcadio/query.rb~ +48 -17
- data/lib/lafcadio/schema.rb~ +56 -0
- data/lib/lafcadio/test.rb +218 -0
- data/lib/lafcadio/test.rb~ +25 -0
- data/lib/lafcadio/test/testconfig.dat~ +13 -0
- data/lib/lafcadio/util.rb +3 -2
- data/lib/lafcadio/util.rb~ +104 -0
- metadata +111 -97
data/lib/lafcadio/objectStore.rb
CHANGED
@@ -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 =~ /
|
84
|
-
|
82
|
+
aString =~ /db_conn:/
|
83
|
+
db_conn_str = $'
|
85
84
|
begin
|
86
|
-
|
85
|
+
db_conn = Marshal.load db_conn_str
|
87
86
|
rescue TypeError
|
88
|
-
|
87
|
+
db_conn = nil
|
89
88
|
end
|
90
|
-
|
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 )
|
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 <<
|
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
|
-
|
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
|
-
|
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
|