-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathOMA.java
486 lines (434 loc) · 19.7 KB
/
OMA.java
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
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.db.jdbc;
import sirius.db.jdbc.constraints.SQLConstraint;
import sirius.db.jdbc.constraints.SQLFilterFactory;
import sirius.db.jdbc.schema.Schema;
import sirius.db.mixing.BaseMapper;
import sirius.db.mixing.EntityDescriptor;
import sirius.db.mixing.IntegrityConstraintFailedException;
import sirius.db.mixing.Mapping;
import sirius.db.mixing.OptimisticLockException;
import sirius.db.mixing.Property;
import sirius.db.mongo.SecondaryCapableMapper;
import sirius.kernel.async.Future;
import sirius.kernel.commons.Context;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.commons.Value;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.Log;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
/**
* Provides the {@link BaseMapper mapper} used to communicate with JDBC / SQL databases.
*/
@Register(classes = OMA.class)
public class OMA extends SecondaryCapableMapper<SQLEntity, SQLConstraint, SmartQuery<? extends SQLEntity>> {
/**
* Contains the central logger for the OMA facility.
*/
public static final Log LOG = Log.get("oma");
/**
* Constrains the factory used to generate filters for a {@link SmartQuery}.
*/
public static final SQLFilterFactory FILTERS = new SQLFilterFactory();
private static final String SQL_WHERE_ID = " WHERE id = ?";
private static final String SQL_AND_VERSION = " AND version = ?";
@Part
private Schema schema;
@Part
private Databases dbs;
private Boolean ready;
/**
* Provides the underlying database instance used to perform the actual statements.
* <p>
* Note that there is a helper available which can generate efficient UPDATE statements which are bound
* to various conditions. See {@link #updateStatement(Class)} for further information.
*
* @param realm the realm to determine the database for
* @return the database used by the framework
*/
@Nullable
public Database getDatabase(String realm) {
return schema.getDatabase(realm);
}
/**
* Provides the underlying database instance used to perform the actual statements.
* <p>
* Note that there is a helper available which can generate efficient UPDATE statements which are bound
* to various conditions. See {@link #updateStatement(Class)} for further information.
*
* @param entityType the entity to determine the database for
* @return the database used by the framework
*/
@Nullable
public Database getDatabase(Class<? extends SQLEntity> entityType) {
return getDatabase(mixing.getDescriptor(entityType).getRealm());
}
/**
* Provides the underlying database instance which represents the local secondary copy of the main database.
* <p>
* In large environments the underlying JDBC database might be setup as a master-slave replication. Such a slave
* is called a secondary copy of the database (as it might not always be fully up-to-date). However, for some
* queries this is sufficient. Also, querying a local copy is faster and takes load from the main database.
*
* @param realm the realm to determine the database for
* @return the secondary database used by the framework. If no secondary database is present or its usage is
* disabled, the primary database is returned.
*/
@Nullable
public Database getSecondaryDatabase(String realm) {
Tuple<Database, Database> primaryAndSecondary = schema.getDatabases(realm).orElse(null);
if (primaryAndSecondary == null) {
return null;
}
if (primaryAndSecondary.getSecond() != null) {
return primaryAndSecondary.getSecond();
}
return primaryAndSecondary.getFirst();
}
/**
* Provides the underlying database instance which represents the local secondary copy of the main database.
* <p>
* In large environments the underlying JDBC database might be setup as a master-slave replication. Such a slave
* is called a secondary copy of the database (as it might not always be fully up-to-date). However, for some
* queries this is sufficient. Also, querying a local copy is faster and takes load from the main database.
*
* @param entityType the entity to determine the database for
* @return the secondary database used by the framework. If no secondary database is present or its usage is
* disabled, the primary database is returned.
*/
@Nullable
public Database getSecondaryDatabase(Class<? extends SQLEntity> entityType) {
return getSecondaryDatabase(mixing.getDescriptor(entityType).getRealm());
}
/**
* Provides a {@link Future} which is fulfilled once the framework is ready.
*
* @return a future which can be used to delay startup actions until the framework is fully functional.
*/
public Future getReadyFuture() {
return schema.getReadyFuture();
}
/**
* Determines if the framework is ready yet.
*
* @return <tt>true</tt> if the framework is ready, <tt>false</tt> otherwise.
*/
public boolean isReady() {
if (ready == null) {
if (schema == null) {
return false;
}
ready = Boolean.FALSE;
getReadyFuture().onSuccess(ignored -> ready = Boolean.TRUE);
}
return ready.booleanValue();
}
/**
* Creates an UPDATE statement which can update one or more fields based on a given set of constraints.
* <p>
* This should be used to generate efficient UPDATE statements with nearly no framework overhead (this
* is essentially a build for a prepared statement).
*
* @param entityType the type to update
* @return the statement builder
*/
public UpdateStatement updateStatement(Class<? extends SQLEntity> entityType) {
EntityDescriptor descriptor = mixing.getDescriptor(entityType);
return new UpdateStatement(descriptor, getDatabase(descriptor.getRealm()));
}
/**
* Creates a DELETE statement which delete entities based on a given set of constraints.
* <p>
* This should be used to generate efficient DELETE statements with nearly no framework overhead (this
* is essentially a build for a prepared statement).
*
* @param entityType the type to delete
* @return the statement builder
*/
public DeleteStatement deleteStatement(Class<? extends SQLEntity> entityType) {
EntityDescriptor descriptor = mixing.getDescriptor(entityType);
return new DeleteStatement(descriptor, getDatabase(descriptor.getRealm()));
}
@Override
protected void createEntity(SQLEntity entity, EntityDescriptor entityDescriptor) throws Exception {
Context insertData = Context.create();
for (Property property : entityDescriptor.getProperties()) {
if (!SQLEntity.ID.getName().equals(property.getName())) {
insertData.set(property.getPropertyName(), property.getValueForDatasource(OMA.class, entity));
}
}
if (entityDescriptor.isVersioned()) {
insertData.set(VERSION, 1);
}
try {
Row keys =
getDatabase(entityDescriptor.getRealm()).insertRow(entityDescriptor.getRelationName(), insertData);
loadCreatedId(entity, keys);
entity.setVersion(1);
} catch (SQLIntegrityConstraintViolationException exception) {
throw new IntegrityConstraintFailedException(exception);
}
}
/**
* Loads an auto generated ID from the given row.
*
* @param entity the target entity to write the ID to
* @param keys the row to read the ID from
*/
public static void loadCreatedId(SQLEntity entity, Row keys) {
if (keys.hasValue("id")) {
// Normally the name of the generated column is reported
entity.setId(keys.getValue("id").asLong(-1));
} else if (keys.hasValue("GENERATED_KEY")) {
// however MySQL reports "GENERATED_KEY"...
entity.setId(keys.getValue("GENERATED_KEY").asLong(-1));
} else if (keys.hasValue("INSERT_ID")) {
// and MariaDB reports "INSERT_ID"...
entity.setId(keys.getValue("INSERT_ID").asLong(-1));
}
}
@Override
protected void updateEntity(SQLEntity entity, boolean force, EntityDescriptor entityDescriptor) throws Exception {
StringBuilder sql = new StringBuilder("UPDATE ");
sql.append(entityDescriptor.getRelationName());
sql.append(" SET ");
List<Object> data = buildUpdateStatement(entity, entityDescriptor, sql);
if (data.isEmpty()) {
return;
}
if (entityDescriptor.isVersioned()) {
if (!data.isEmpty()) {
sql.append(",");
}
sql.append("version = ? ");
}
sql.append(SQL_WHERE_ID);
if (entityDescriptor.isVersioned() && !force) {
sql.append(SQL_AND_VERSION);
}
executeUPDATE(entity, entityDescriptor, force, sql.toString(), data);
}
private List<Object> buildUpdateStatement(SQLEntity entity, EntityDescriptor entityDescriptor, StringBuilder sql) {
List<Object> data = new ArrayList<>();
for (Property property : entityDescriptor.getProperties()) {
if (entityDescriptor.isChanged(entity, property)) {
if (SQLEntity.ID.getName().equals(property.getName())) {
throw new IllegalStateException("The id column of an entity must not be modified manually!");
}
if (!data.isEmpty()) {
sql.append(", ");
}
sql.append(property.getPropertyName());
sql.append(" = ? ");
data.add(property.getValueForDatasource(OMA.class, entity));
}
}
return data;
}
private void executeUPDATE(SQLEntity entity,
EntityDescriptor entityDescriptor,
boolean force,
String sql,
List<Object> data)
throws SQLException, OptimisticLockException, IntegrityConstraintFailedException {
try (Connection connection = getDatabase(entityDescriptor.getRealm()).getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
int index = 1;
for (Object object : data) {
Databases.convertAndSetParameter(statement, index++, object);
}
if (entityDescriptor.isVersioned()) {
statement.setInt(index++, entity.getVersion() + 1);
}
statement.setLong(index++, entity.getId());
if (entityDescriptor.isVersioned() && !force) {
statement.setInt(index++, entity.getVersion());
}
int updatedRows = statement.executeUpdate();
enforceUpdate(entity, force, updatedRows);
if (entityDescriptor.isVersioned()) {
entity.setVersion(entity.getVersion() + 1);
}
}
} catch (SQLIntegrityConstraintViolationException exception) {
throw new IntegrityConstraintFailedException(exception);
}
}
private void enforceUpdate(SQLEntity entity, boolean force, int updatedRows) throws OptimisticLockException {
if (force || updatedRows > 0) {
return;
}
if (find(entity.getClass(), entity.getId()).isPresent()) {
throw new OptimisticLockException();
} else {
throw Exceptions.handle()
.to(OMA.LOG)
.withSystemErrorMessage(
"The entity %s (%s) cannot be updated as it does not exist in the database!",
entity,
entity.getId())
.handle();
}
}
@Override
protected void deleteEntity(SQLEntity entity, boolean force, EntityDescriptor entityDescriptor) throws Exception {
StringBuilder sql = new StringBuilder("DELETE FROM ");
sql.append(entityDescriptor.getRelationName());
sql.append(SQL_WHERE_ID);
if (entityDescriptor.isVersioned() && !force) {
sql.append(SQL_AND_VERSION);
}
try (Connection connection = getDatabase(entityDescriptor.getRealm()).getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(sql.toString())) {
statement.setLong(1, entity.getId());
if (entityDescriptor.isVersioned() && !force) {
statement.setInt(2, entity.getVersion());
}
int updatedRows = statement.executeUpdate();
if (updatedRows == 0 && find(entity.getClass(), entity.getId()).isPresent()) {
throw new OptimisticLockException();
}
}
}
}
@Override
public <E extends SQLEntity> SmartQuery<E> select(Class<E> type) {
EntityDescriptor entityDescriptor = mixing.getDescriptor(type);
return new SmartQuery<>(entityDescriptor, getDatabase(entityDescriptor.getRealm()));
}
@Override
public <E extends SQLEntity> SmartQuery<E> selectFromSecondary(Class<E> type) {
EntityDescriptor entityDescriptor = mixing.getDescriptor(type);
return new SmartQuery<>(entityDescriptor, getSecondaryDatabase(entityDescriptor.getRealm()));
}
@Override
public SQLFilterFactory filters() {
return FILTERS;
}
/**
* Transforms a plain {@link SQLQuery} to directly return entities of the given type.
*
* @param type the type of entities to read from the query result
* @param query the query to transform
* @param <E> the generic type of entities to read from the query result
* @return a transformed query which returns entities instead of result rows.
*/
public <E extends SQLEntity> TransformedQuery<E> transform(Class<E> type, SQLQuery query) {
return new TransformedQuery<>(mixing.getDescriptor(type), null, query);
}
/**
* Same as {@link #transform(Class, SQLQuery)} but with support for aliased columns.
* <p>
* If the columns to read from the result set are aliased, this method can be used to specify the alias to use.
*
* @param type the type of entities to read from the query result
* @param alias the alias which is appended to all column names to read
* @param query the query to transform
* @param <E> the generic type of entities to read from the query result
* @return a transformed query which returns entities instead of result rows.
*/
public <E extends SQLEntity> TransformedQuery<E> transform(Class<E> type, String alias, SQLQuery query) {
return new TransformedQuery<>(mixing.getDescriptor(type), alias, query);
}
/**
* Tries to find the entity with the given ID
*
* @param id the ID of the entity to find
* @param entityDescriptor the descriptor of the entity to find
* @param context the advanced search context which can be populated using
* {@link sirius.db.mixing.ContextInfo context info elements}.
* @param <E> the generic type of the entity tp find
* @return the entity wrapped as optional or an empty optional if no entity was found
* @throws Exception in case of a database or system error
*/
@Override
protected <E extends SQLEntity> Optional<E> findEntity(Object id,
EntityDescriptor entityDescriptor,
Function<String, Value> context) throws Exception {
if (context.apply(SecondaryCapableMapper.CONTEXT_IN_SECONDARY).asBoolean(false)) {
try (Connection connection = getSecondaryDatabase(entityDescriptor.getRealm()).getConnection()) {
return execFind(id, entityDescriptor, connection);
}
}
try (Connection connection = getDatabase(entityDescriptor.getRealm()).getConnection()) {
return execFind(id, entityDescriptor, connection);
}
}
@SuppressWarnings("unchecked")
protected <E extends SQLEntity> Optional<E> execFind(Object id,
EntityDescriptor entityDescriptor,
Connection connection) throws Exception {
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM "
+ entityDescriptor.getRelationName()
+ SQL_WHERE_ID,
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY)) {
statement.setLong(1, Value.of(id).asLong(-1));
try (ResultSet result = statement.executeQuery()) {
if (!result.next()) {
return Optional.empty();
}
Set<String> columns = dbs.readColumns(result);
E entity = (E) entityDescriptor.make(OMA.class, null, key -> {
String effectiveKey = key.toUpperCase();
if (!columns.contains(effectiveKey)) {
return null;
}
try {
return Value.of(result.getObject(effectiveKey));
} catch (SQLException exception) {
throw Exceptions.handle(OMA.LOG, exception);
}
});
if (entityDescriptor.isVersioned()) {
entity.setVersion(result.getInt(BaseMapper.VERSION.toUpperCase()));
}
return Optional.of(entity);
}
}
}
@SuppressWarnings("unchecked")
@Override
protected <E extends SQLEntity> Optional<E> findEntity(E entity) {
return find((Class<E>) entity.getClass(), entity.getId());
}
@Override
public Value fetchField(Class<? extends SQLEntity> type, Object id, Mapping field) throws Exception {
if (Strings.isEmpty(id)) {
return Value.EMPTY;
}
return select(type).fields(field)
.limit(1)
.eq(SQLEntity.ID, id)
.asSQLQuery()
.first()
.map(row -> Value.of(mixing.getDescriptor(type)
.getProperty(field)
.transformFromDatasource(getClass(),
row.getValue(field.toString()))))
.orElse(Value.EMPTY);
}
@Override
protected int determineRetryTimeoutFactor() {
return 50;
}
}