forked from headius/jibernate
/
dm-hibernate-adapter.rb
488 lines (406 loc) · 16.8 KB
/
dm-hibernate-adapter.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
require 'java'
require 'pathname'
# 3.6.1
# HIBERNATE_LIBS = %w(antlr-2.7.6.jar dom4j-1.6.1.jar javassist-3.12.0.GA.jar jta-1.1.jar
# log4j-1.2.14.jar slf4j-api-1.6.1.jar hibernate3.jar hibernate-jpa-2.0-api-1.0.0.Final.jar)
HIBERNATE_LIBS = %w(hibernate-core-3.3.2.GA.jar log4j-1.2.14.jar hibernate-tools-3.2.4.GA.jar slf4j-log4j12-1.5.2.jar
slf4j-api-1.5.2.jar javassist-3.8.0.GA.jar hibernate-annotations-3.4.0.GA.jar jta-1.1.jar
antlr-2.7.6.jar dom4j-1.6.1.jar hibernate-commons-annotations-3.3.0.ga.jar ejb3-persistence-3.3.2.Beta1.jar
commons-collections-3.1.jar)
JDBC_DRIVERS = %w(h2-1.3.148.jar)
base_path = Pathname(__FILE__).dirname.expand_path
dir = "#{base_path}/dm-hibernate-adapter"
(JDBC_DRIVERS + HIBERNATE_LIBS).each do |lib_name|
require "#{base_path}/#{lib_name}"
end
begin
require 'dm-hibernate-adapter_ext.jar'
rescue LoadError
warn "missing extension jar, may it is already in the parent classloader"
end
import 'de.saumya.jibernate.UpdateWork'
require 'slf4r'
if require 'dm-core'
DataMapper.logger = Slf4r::LoggerFacade.new(DataMapper)
end
require 'dm-core/adapters/abstract_adapter'
require 'jruby/core_ext'
require 'stringio'
require "#{dir}/dialects"
require "#{dir}/hibernate"
require "#{dir}/transaction"
module DataMapper
module Adapters
java_import org.hibernate.criterion.Restrictions # ie. Restriction.eq
java_import org.hibernate.criterion.Order # ie. Order.asc
class HibernateAdapter < AbstractAdapter
@@logger = Slf4r::LoggerFacade.new(HibernateAdapter)
DRIVERS = {
:H2 => "org.h2.Driver",
:HSQL => "org.hsqldb.jdbcDriver",
:Derby => "org.apache.derby.jdbc.EmbeddedDriver",
:MySQL5 => "com.mysql.jdbc.Driver",
:MySQL5InnoDB => "com.mysql.jdbc.Driver",
:MySQL => "com.mysql.jdbc.Driver",
:MySQLInnoDB => "com.mysql.jdbc.Driver",
:MySQLMyISAM => "com.mysql.jdbc.Driver",
:PostgreSQL => "org.postgresql.Driver",
}
DataMapper::Model.append_inclusions( Hibernate::Model )
extend( Chainable )
def initialize(name, options = {})
dialect = options.delete(:dialect)
username = options.delete(:username)
password = options.delete(:password)
driver = options.delete(:driver) || DRIVERS[dialect.to_sym]
pool_size = options.delete(:pool_size) || "1"
url = options.delete(:url)
url += "jdbc:" unless url =~ /^jdbc:/
super( name, options )
Hibernate.dialect = Hibernate::Dialects.const_get(dialect.to_s)
Hibernate.current_session_context_class = "thread"
Hibernate.connection_driver_class = driver.to_s
Hibernate.connection_url = url.to_s # ie. "jdbc:h2:jibernate"
Hibernate.connection_username = username.to_s # ie. "sa"
Hibernate.connection_password = password.to_s # ie. ""
Hibernate.connection_pool_size = pool_size.to_s
Hibernate.properties["cache.provider_class"] = "org.hibernate.cache.NoCacheProvider"
Hibernate.properties["hbm2ddl.auto"] = "update"
Hibernate.properties["format_sql"] = "false"
Hibernate.properties["show_sql"] = "true"
end
# @param [Enumerable<Resource>] resources
# The list of resources (model instances) to create
#
# @return [Integer]
# The number of records that were actually saved into the data-store
#
# @api semipublic
def create(resources)
@@logger.debug("create #{resources.inspect}")
count = 0
unit_of_work do |session|
resources.each do |resource|
begin
session.persist(resource)
count += 1
rescue NativeException => e
@@logger.debug("error creating #{resource.inspect()}", e.cause())
session.clear()
raise e
end
end
end
count
end
# @param [Hash(Property => Object)] attributes
# hash of attribute values to set, keyed by Property
# @param [Collection] collection
# collection of records to be updated
#
# @return [Integer]
# the number of records updated
#
# @api semipublic
def update(attributes, collection)
log_update(attributes, collection)
count = 0
unit_of_work do |session|
collection.each do |resource|
session.update(resource)
count += 1
end
end
count
end
# @param [Query] query
# the query to match resources in the datastore
#
# @return [Enumerable<Hash>]
# an array of hashes to become resources
#
# @api semipublic
def read(query)
log_read(query)
conditions = query.conditions
model = query.model
limit = query.limit
offset = query.offset
order = query.order
result = []
unit_of_work do |session|
criteria = session.create_criteria(model.to_java_class_name)
# where ...
criteria.add(parse_conditions_tree(conditions,model)) unless conditions.nil?
# limit ...
criteria.set_max_results(limit) unless limit.nil?
# offset
criteria.set_first_result(offset) unless offset.nil?
# order by
order.each do |direction|
operator = direction.operator
# TODO column name may differ from property name
column = direction.target.name
if operator == :desc
order = Order.desc(column.to_s.to_java_string)
else
order = Order.asc(column.to_s.to_java_string)
end
criteria.add_order(order)
end
@@logger.debug(criteria.to_s)
# TODO handle exceptions
result = criteria.list
end
result.to_a
end
# @param [Collection] collection
# collection of records to be deleted
#
# @return [Integer]
# the number of records deleted
#
# @api semipublic
def delete(resources)
unit_of_work do |session|
resources.each do |resource|
@@logger.debug("deleting #{resource.inspect}")
session.delete(resource)
end
end
resources.size
end
# extension to the adapter API
def execute_update(sql)
unit_of_work do |session|
session.do_work(UpdateWork.new(sql))
end
end
# <dm-transactions>
# Produces a fresh transaction primitive for this Adapter
#
# Used by Transaction to perform its various tasks.
#
# @return [Object]
# a new Object that responds to :close, :begin, :commit,
# and :rollback,
#
# @api private
def transaction_primitive()
# DataObjects::Transaction.create_for_uri(normalized_uri)
Hibernate::Transaction.new()
end
# Pushes the given Transaction onto the per thread Transaction stack so
# that everything done by this Adapter is done within the context of said
# Transaction.
#
# @param [Transaction] transaction
# a Transaction to be the 'current' transaction until popped.
#
# @return [Array(Transaction)]
# the stack of active transactions for the current thread
#
# @api private
#
def push_transaction(transaction)
transactions() << transaction
end
# Pop the 'current' Transaction from the per thread Transaction stack so
# that everything done by this Adapter is no longer necessarily within the
# context of said Transaction.
#
# @return [Transaction]
# the former 'current' transaction.
#
# @api private
def pop_transaction()
transactions().pop()
end
# Retrieve the current transaction for this Adapter.
#
# Everything done by this Adapter is done within the context of this
# Transaction.
#
# @return [Transaction]
# the 'current' transaction for this Adapter.
#
# @api private
def current_transaction()
transactions().last()
end
# </dm-transactions>
private
# @api private
def transactions()
Thread.current[:dm_transactions] ||= {}
Thread.current[:dm_transactions][object_id] ||= []
end
def unit_of_work( &block )
# TODO state of the session should be also checked!
current_tx = current_transaction()
if current_tx
block.call( current_tx.primitive_for( self ).session() )
else
Hibernate.tx( &block )
end
end
def cast_to_hibernate (value, model_type)
#TODO ADD MORE TYPES!!!
case value
when Fixnum
# XXX Warning. ie Integer.value_of(value) returns cached objects already converted to Ruby objects!
if model_type == Java::JavaLang::Integer then java.lang.Integer.new(value)
elsif model_type == Java::JavaLang::Long then java.lang.Long.new(value)
else puts "---other Hibernate type, object: #{value} type: #{value.class} Hibernate type: #{model_type} ---"
end
when Float then java.lang.Float.new(value)
when String then value.to_java_string
when Array
# if there is WHERE x IN ( ) -> WHERE x IN ( null ) should be used
value = [nil] if value.empty?
(value.map{|object| cast_to_hibernate(object, model_type)}).to_java
when Range then (value.to_a.map{|object| cast_to_hibernate(object, model_type)}).to_java
when NilClass then nil
when Regexp then value.source.to_java_string
else
puts "---other Ruby type, object: #{value} type: #{value.class} ---"
value.to_s.to_java_string
end
end
def handle_comparison(con, model)
subject = nil
value = nil
case con.subject
when DataMapper::Property
subject = con.subject.name.to_s # property/column name
value = con.value # value used in comparison
when DataMapper::Associations::ManyToOne::Relationship
# TODO allow multicolumn keys !!!
subject = con.subject.parent_key.first.name.to_s
value = con.subject.parent_key.get(con.value).first # value used in comparison
when DataMapper::Associations::OneToMany::Relationship
# TODO allow multicolumn keys !!!
# TODO why the break in symetry ?
subject = con.subject.parent_key.first.name.to_s
# why does is not work: con.subject.child_key.get(con.value).first ???
value = con.subject.child_key.first.get(con.value.first) # value used in comparison
end
model_type = model.to_java_type(model.properties[subject.to_sym].class) # Java type of property (used in typecasting)
dialect = Hibernate.dialect # SQL dialect for current configuration
case con
when DataMapper::Query::Conditions::EqualToComparison
# special case handling IS NULL/ NOT (x IS NULL)
value.class == NilClass ? Restrictions.isNull(subject) :
Restrictions.eq(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::GreaterThanComparison
Restrictions.gt(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::LessThanComparison
Restrictions.lt(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::LikeComparison
Restrictions.like(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::GreaterThanOrEqualToComparison
Restrictions.ge(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::LessThanOrEqualToComparison
Restrictions.le(subject, cast_to_hibernate(value, model_type))
when DataMapper::Query::Conditions::InclusionComparison
if value.class == Array
# special case handling :x => 1..110 / :x => [1,2,3]
Restrictions.in(subject, cast_to_hibernate(value, model_type))
else
# XXX proper ordering?
arr = value.is_a?(Fixnum) ? [value] : value.to_a
lo = arr.first
hi = arr.last
if lo.nil? || hi.nil?
Restrictions.in(subject, cast_to_hibernate(value, model_type))
else
Restrictions.between(subject, cast_to_hibernate(lo, model_type), cast_to_hibernate(hi, model_type))
end
end
when DataMapper::Query::Conditions::RegexpComparison
if dialect == "org.hibernate.dialect.HSQLDialect"
Restrictions.sqlRestriction("(regexp_matches (" +subject + ", ?))",
cast_to_hibernate(value, model_type), org::hibernate::Hibernate::STRING)
elsif dialect == "org.hibernate.dialect.PostgreSQLDialect"
Restrictions.sqlRestriction("(" + subject +" ~ ?)",
cast_to_hibernate(value, model_type), org::hibernate::Hibernate::STRING)
# elsif dialect == "org.hibernate.dialect.DerbyDialect"
# TODO implement custom matching function for some dbs (see README on Wiki)
else
Restrictions.sqlRestriction("(" + subject +" regexp ?)",
cast_to_hibernate(value, model_type), org::hibernate::Hibernate::STRING)
end
end
end
def parse_all_children(children, model, operand)
operand = children.inject(operand){ |op,child| op.add(parse_conditions_tree(child, model))}
end
def parse_the_only_child(child,model)
parse_conditions_tree(child, model)
end
def handle_operation(con, model)
children = con.children
case con
when DataMapper::Query::Conditions::AndOperation
parse_all_children(children, model, Restrictions.conjunction())
when DataMapper::Query::Conditions::OrOperation
parse_all_children(children, model, Restrictions.disjunction())
when DataMapper::Query::Conditions::NotOperation
#XXX only one child may be negated in DM?
child = children.first
if !(child.respond_to? :children) &&
(child.class == DataMapper::Query::Conditions::InclusionComparison) &&
(child.value.class == Array) && (child.value.empty?)
subject = child.subject.name.to_s
# XXX ugly workaround for Model.all(:x.not => [])
Restrictions.sqlRestriction(" ( "+ subject +" is null or " + subject +" is not null ) ")
else
Restrictions.not(parse_the_only_child(child,model))
end
when DataMapper::Query::Conditions::NullOperation
# XXX NullOperation is not used in dm_core at the moment
raise NotImplementedError, "#{con.class} is not not used in dm_core"
end
end
def parse_conditions_tree(conditions, model)
#conditions has children ? (in fact -> "is it comparison or operand?")
unless conditions.respond_to?(:children)
handle_comparison(conditions, model)
else
handle_operation(conditions, model)
end
end
# ----- helper methods - printers -----
# @param [Query] query
# the query to print it out formatted
#
# @api private
def log_read(query)
@@logger.debug <<-EOT
read()
query:
#{query.inspect}
model:
#{query.model}
conditions:
#{query.conditions}
EOT
end
# @param [Hash(Property => Object)] attributes
# hash of attribute values to print it out formatted, keyed by Property
# @param [Collection] collection
# collection of records to print it out formatted
#
# @api private
def log_update(attributes,collection)
@@logger.debug <<-EOT
update()
attributes:
#{attributes.inspect}
collection:
#{collection.inspect}
EOT
end
end
end
end