-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathBaseMapper.java
647 lines (591 loc) · 25.4 KB
/
BaseMapper.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
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
/*
* 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.mixing;
import sirius.db.jdbc.SQLEntity;
import sirius.db.mixing.annotations.Versioned;
import sirius.db.mixing.query.Query;
import sirius.db.mixing.query.constraints.Constraint;
import sirius.db.mixing.query.constraints.FilterFactory;
import sirius.db.mixing.types.BaseEntityRef;
import sirius.kernel.Sirius;
import sirius.kernel.async.TaskContext;
import sirius.kernel.commons.Callback;
import sirius.kernel.commons.Explain;
import sirius.kernel.commons.Monoflop;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.commons.UnitOfWork;
import sirius.kernel.commons.Value;
import sirius.kernel.commons.Wait;
import sirius.kernel.commons.Watch;
import sirius.kernel.di.std.Part;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.HandledException;
import javax.annotation.CheckReturnValue;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
/**
* Declares the common functionality of a mapper which is responsible for storing and loading entities to and from a database.
*
* @param <B> the type of entities supported by this mapper
* @param <C> the type of constraints supported by this mapper
* @param <Q> the type of queries supported by this mapper
*/
public abstract class BaseMapper<B extends BaseEntity<?>, C extends Constraint, Q extends Query<?, ? extends B, C>> {
private static final Function<String, Value> EMPTY_CONTEXT = key -> Value.EMPTY;
private static final String TIMING_CATEGORY_MIXING = "MIXING";
/**
* Contains the name of the version column used for optimistic locking.
*/
public static final String VERSION = "version";
@Part
protected Mixing mixing;
/**
* Writes the contents of the given entity to the database.
* <p>
* If the entity is not persisted yet, we perform an insert. If the entity does exist, we only
* update those fields, which were changed since they were last read from the database.
* <p>
* While this provides the best performance and circumvents update conflicts, it does not guarantee strong
* consistency as the fields in the database might have partially changes. If this behaviour is unwanted,
* {@link Versioned} can be used which will turn on <tt>Optimistic Locking</tt> and
* prevent these conditions.
*
* @param entity the entity to write to the database
* @param <E> the generic type of the entity
*/
public <E extends B> void update(E entity) {
try {
performUpdate(entity, false);
} catch (OptimisticLockException | IntegrityConstraintFailedException exception) {
throw Exceptions.handle(exception);
}
}
/**
* Tries to perform an {@link #update(BaseEntity)} of the given entity.
* <p>
* If the entity is {@link Versioned} and the entity was modified already elsewhere, an
* {@link OptimisticLockException} will be thrown, which can be used to trigger a retry.
*
* @param entity the entity to update
* @param <E> the generic type of the entity
* @throws OptimisticLockException in case of a concurrent modification
* @throws IntegrityConstraintFailedException in case of a failed integrity constraint as signaled by the database
*/
@SuppressWarnings("squid:S1160")
@Explain("In this case we want to throw two distinct exceptions to differentiate between our optimistic locking "
+ "and database supported OL")
public <E extends B> void tryUpdate(E entity) throws OptimisticLockException, IntegrityConstraintFailedException {
performUpdate(entity, false);
}
/**
* Tries to apply the given changes and to save the resulting entity.
* <p>
* Tries to perform the given modifications and then to update the entity. If an optimistic lock error occurs,
* the entity is refreshed and the modifications are re-executed along with another update.
*
* @param entity the entity to update
* @param preSaveModifier the changes to perform on the entity
* @param <E> the type of the entity to update
* @throws HandledException if either any other exception occurs, or if all three attempts fail with an optimistic
* lock error.
*/
public <E extends B> void retryUpdate(E entity, Callback<E> preSaveModifier) {
Monoflop retryOccurred = Monoflop.create();
retry(() -> {
E entityToUpdate = entity;
if (retryOccurred.successiveCall()) {
entityToUpdate = tryRefresh(entity);
}
preSaveModifier.invoke(entityToUpdate);
tryUpdate(entityToUpdate);
});
}
/**
* Handles the given unit of work while restarting it if an optimistic lock error occurs.
*
* @param unitOfWork the unit of work to handle.
* @throws HandledException if either any other exception occurs, or if all three attempts
* fail with an optimistic lock error.
*/
public void retry(UnitOfWork unitOfWork) {
int retries = 3;
while (retries > 0) {
retries--;
try {
unitOfWork.execute();
return;
} catch (OptimisticLockException optimisticLockException) {
Mixing.LOG.FINE(optimisticLockException);
if (Sirius.isDev()) {
Mixing.LOG.INFO("Retrying due to optimistic lock: %s", optimisticLockException);
}
if (retries <= 0) {
throw Exceptions.handle()
.withSystemErrorMessage(
"Failed to update an entity after re-trying a unit of work several times: %s (%s)")
.error(optimisticLockException)
.to(Mixing.LOG)
.handle();
}
int timeoutFactor = determineRetryTimeoutFactor();
// Wait 0, x ms, 2*x ms
Wait.millis((2 - retries) * timeoutFactor);
// Wait 0...x ms in 50% of all calls...
Wait.randomMillis(-timeoutFactor, timeoutFactor);
} catch (HandledException handledException) {
throw handledException;
} catch (Exception exception) {
throw Exceptions.handle()
.withSystemErrorMessage(
"An unexpected exception occurred while executing a unit of work: %s (%s)")
.error(exception)
.to(Mixing.LOG)
.handle();
}
}
}
/**
* Determines the factor in ms to be used by {@link #retry} for specifying how long should be waited between retries.
*
* @return an amount of ms which should be used as basis for calculating the retry timeout
*/
protected abstract int determineRetryTimeoutFactor();
/**
* Performs an {@link #update(BaseEntity)} of the entity, without checking for concurrent modifications.
* <p>Concurrent modifications by other users will simply be ignored and overridden.
*
* @param entity the entity to update
* @param <E> the generic type of the entity
*/
public <E extends B> void override(E entity) {
try {
performUpdate(entity, true);
} catch (IntegrityConstraintFailedException | OptimisticLockException exception) {
throw Exceptions.handle(exception);
}
}
@SuppressWarnings("squid:RedundantThrowsDeclarationCheck")
@Explain("false positive - both exceptions can be thrown")
protected <E extends B> void performUpdate(E entity, boolean force)
throws OptimisticLockException, IntegrityConstraintFailedException {
if (entity == null) {
return;
}
try {
EntityDescriptor entityDescriptor = entity.getDescriptor();
invokeBeforeSaveHandlers(entity, entityDescriptor);
if (entity.isNew()) {
createEntity(entity, entityDescriptor);
} else {
updateEntity(entity, force, entityDescriptor);
}
invokeAfterSaveHandlers(entity, entityDescriptor);
} catch (IntegrityConstraintFailedException | OptimisticLockException exception) {
throw exception;
} catch (Exception exception) {
throw Exceptions.handle()
.to(Mixing.LOG)
.error(exception)
.withSystemErrorMessage("Unable to UPDATE %s (%s): %s (%s)",
entity,
entity.getClass().getSimpleName())
.handle();
}
}
private <E extends B> void invokeBeforeSaveHandlers(E entity, EntityDescriptor entityDescriptor) {
Watch watch = Watch.start();
try {
entityDescriptor.beforeSave(entity);
} finally {
watch.submitMicroTiming(TIMING_CATEGORY_MIXING, "BeforeSave - " + entityDescriptor.getName());
}
}
private <E extends B> void invokeAfterSaveHandlers(E entity, EntityDescriptor entityDescriptor) {
Watch watch = Watch.start();
try {
entityDescriptor.afterSave(entity);
} finally {
watch.submitMicroTiming(TIMING_CATEGORY_MIXING, "AfterSave - " + entityDescriptor.getName());
}
}
/**
* Creates a new entity in the underlying database.
*
* @param entity the entity to create
* @param entityDescriptor the descriptor of the entity
* @throws Exception in case of a database error
*/
protected abstract void createEntity(B entity, EntityDescriptor entityDescriptor) throws Exception;
/**
* Updates an existing entity in the underlying database.
*
* @param entity the entity to update
* @param force <tt>ture</tt> if the update is forced and optimistic locking must be disabled
* @param entityDescriptor the descriptor of the entity
* @throws Exception in case of a database error
*/
protected abstract void updateEntity(B entity, boolean force, EntityDescriptor entityDescriptor) throws Exception;
/**
* Deletes the given entity from the database.
* <p>
* If the entity is {@link Versioned} and concurrently modified elsewhere,
* an exception is thrown.
*
* @param entity the entity to delete
* @param <E> the generic entity type
*/
public <E extends B> void delete(E entity) {
try {
performDelete(entity, false);
} catch (OptimisticLockException exception) {
throw Exceptions.handle(exception);
}
}
/**
* Tries to delete the entity from the database.
* <p>
* If the entity is {@link Versioned} and concurrently modified elsewhere,
* an {@link OptimisticLockException} is thrown.
*
* @param entity the entity to delete
* @param <E> the generic entity type
* @throws OptimisticLockException if the entity was concurrently modified
*/
public <E extends B> void tryDelete(E entity) throws OptimisticLockException {
performDelete(entity, false);
}
/**
* Deletes the given entity from the database even if it is {@link Versioned} and was
* concurrently modified.
*
* @param entity the entity to delete
* @param <E> the generic entity type
*/
public <E extends B> void forceDelete(E entity) {
try {
performDelete(entity, true);
} catch (OptimisticLockException exception) {
// Should really not happen....
throw Exceptions.handle(exception);
}
}
protected <E extends B> void performDelete(E entity, boolean force) throws OptimisticLockException {
if (entity == null || entity.isNew()) {
return;
}
try {
EntityDescriptor entityDescriptor = entity.getDescriptor();
invokeBeforeDeleteHandlers(entity, entityDescriptor);
if (TaskContext.get().isActive()) {
deleteEntity(entity, force, entityDescriptor);
invokeAfterDeleteHandlers(entity, entityDescriptor);
}
} catch (OptimisticLockException exception) {
throw exception;
} catch (Exception exception) {
throw Exceptions.handle()
.to(Mixing.LOG)
.error(exception)
.withSystemErrorMessage("Unable to DELETE %s (%s): %s (%s)",
entity,
entity.getClass().getSimpleName())
.handle();
}
}
/**
* Asserts that the given entity is deletable.
* <p>
* This effecifely checks if the entity is referenced by other entities using {@link BaseEntityRef.OnDelete#REJECT}.
*
* @param entity the entity to check
* @param <E> the generic entity type
*/
public <E extends B> void assertDeletable(E entity) {
if (entity == null || entity.isNew()) {
return;
}
EntityDescriptor entityDescriptor = entity.getDescriptor();
Watch watch = Watch.start();
try {
entityDescriptor.invokeRejectDeleteHandlers(entity);
} catch (Exception exception) {
throw Exceptions.handle()
.to(Mixing.LOG)
.error(exception)
.withSystemErrorMessage("Unable to DELETE %s (%s): %s (%s)",
entity,
entity.getClass().getSimpleName())
.handle();
} finally {
watch.submitMicroTiming(TIMING_CATEGORY_MIXING, "RejectDelete - " + entityDescriptor.getName());
}
}
private <E extends B> void invokeBeforeDeleteHandlers(E entity, EntityDescriptor entityDescriptor) {
Watch watch = Watch.start();
try {
entityDescriptor.beforeDelete(entity);
} finally {
watch.submitMicroTiming(TIMING_CATEGORY_MIXING, "BeforeDelete - " + entityDescriptor.getName());
}
}
private <E extends B> void invokeAfterDeleteHandlers(E entity, EntityDescriptor entityDescriptor) {
Watch watch = Watch.start();
try {
entityDescriptor.afterDelete(entity);
} finally {
watch.submitMicroTiming(TIMING_CATEGORY_MIXING, "AfterDelete - " + entityDescriptor.getName());
}
}
/**
* Deletes the give entity from the database.
*
* @param entity the entity to delete
* @param force <tt>true</tt> if the deletion is forced and optimistic locking must be disabled
* @param entityDescriptor the descriptor of the entity
* @throws Exception in case of a database error
*/
protected abstract void deleteEntity(B entity, boolean force, EntityDescriptor entityDescriptor) throws Exception;
/**
* Determines if the given entity has validation warnings.
*
* @param entity the entity to check
* @return <tt>true</tt> if there are validation warnings, <tt>false</tt> otherwise
*/
public boolean hasValidationWarnings(B entity) {
if (entity == null) {
return false;
}
EntityDescriptor entityDescriptor = entity.getDescriptor();
return entityDescriptor.hasValidationWarnings(entity);
}
/**
* Executes all validation handlers on the given entity.
*
* @param entity the entity to validate
* @return a list of all validation warnings
*/
public List<String> validate(B entity) {
if (entity == null) {
return Collections.emptyList();
}
EntityDescriptor entityDescriptor = entity.getDescriptor();
return entityDescriptor.validate(entity);
}
/**
* Performs a database lookup to select the entity of the given type with the given ID.
*
* @param type the type of entity to select
* @param id the ID (which can be either a long, int or String) to select
* @param info info provided as context (e.g. routing infos for Elasticsearch)
* @param <E> the generic type of the entity to select
* @return the entity wrapped as <tt>Optional</tt> or an empty optional if no entity with the given ID exists
*/
public <E extends B> Optional<E> find(Class<E> type, Object id, ContextInfo... info) {
try {
if (Strings.isEmpty(id)) {
return Optional.empty();
}
Class<?> clazz = id.getClass();
if (!isPossibleId(clazz)) {
throw Exceptions.handle()
.to(Mixing.LOG)
.withSystemErrorMessage("The given object is not an ID (String, long, int): %s (%s)",
id,
type)
.handle();
}
EntityDescriptor entityDescriptor = mixing.getDescriptor(type);
return findEntity(id, entityDescriptor, makeContext(info));
} catch (HandledException exception) {
throw exception;
} catch (Exception exception) {
throw Exceptions.handle()
.to(Mixing.LOG)
.error(exception)
.withSystemErrorMessage("Unable to FIND %s (%s): %s (%s)", type.getSimpleName(), id)
.handle();
}
}
@SuppressWarnings("java:S1067")
@Explain("We rather keep all possible cases in one place.")
private boolean isPossibleId(Class<?> clazz) {
return clazz == String.class
|| clazz == long.class
|| clazz == Long.class
|| clazz == int.class
|| clazz == Integer.class;
}
private Function<String, Value> makeContext(ContextInfo[] info) {
if (info == null || info.length == 0) {
return EMPTY_CONTEXT;
}
return key -> {
for (int i = 0; i < info.length; i++) {
if (Strings.areEqual(info[i].getKey(), key)) {
return info[i].getValue();
}
}
return Value.EMPTY;
};
}
/**
* 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 ContextInfo} in
* {@link #find(Class, Object, ContextInfo...)}
* @param <E> the effective type of the entity
* @return the entity wrapped as optional or an empty optional if the entity was not found
* @throws Exception in case of a database error
*/
protected abstract <E extends B> Optional<E> findEntity(Object id,
EntityDescriptor entityDescriptor,
Function<String, Value> context) throws Exception;
/**
* Tries to {@link #find(Class, Object, ContextInfo...)} the entity with the given ID.
* <p>
* If no entity is found, an exception is thrown.
*
* @param type the type of entity to select
* @param id the ID (which can be either a long, Long or String) to select
* @param info info provided as context (e.g. routing infos for Elasticsearch)
* @param <E> the generic type of the entity to select
* @return the entity with the given ID
* @throws HandledException if no entity with the given ID was present
*/
public <E extends B> E findOrFail(Class<E> type, Object id, ContextInfo... info) {
Optional<E> result = find(type, id, info);
if (result.isPresent()) {
return result.get();
} else {
throw Exceptions.handle()
.to(Mixing.LOG)
.withSystemErrorMessage("Cannot find entity of type '%s' with ID '%s'", type.getName(), id)
.handle();
}
}
/**
* Tries to resolve the {@link SQLEntity#getUniqueName()} into an entity.
*
* @param name the name of the entity to resolve
* @param info info provided as context (e.g. routing infos for Elasticsearch)
* @param <E> the generic parameter of the entity to resolve
* @return the resolved entity wrapped as <tt>Optional</tt> or an empty optional if no such entity exists
*/
@SuppressWarnings("unchecked")
public <E extends B> Optional<E> resolve(String name, ContextInfo... info) {
if (Strings.isEmpty(name)) {
return Optional.empty();
}
Tuple<String, String> typeAndId = Mixing.splitUniqueName(name);
if (Strings.isEmpty(typeAndId.getSecond())) {
return Optional.empty();
}
return mixing.findDescriptor(typeAndId.getFirst())
.flatMap(descriptor -> find((Class<E>) descriptor.getType(), typeAndId.getSecond(), info));
}
/**
* Tries to {@link #resolve(String, ContextInfo...)} the given name into an entity.
*
* @param name the name of the entity to resolve
* @param info info provided as context (e.g. routing infos for Elasticsearch)
* @return the resolved entity
* @throws HandledException if the given name cannot be resolved into an entity
*/
public B resolveOrFail(String name, ContextInfo... info) {
Optional<? extends B> result = resolve(name, info);
if (result.isPresent()) {
return result.get();
} else {
throw Exceptions.handle()
.to(Mixing.LOG)
.withSystemErrorMessage("Cannot find entity named '%s'", name)
.handle();
}
}
/**
* Tries to fetch a fresh (updated) instance of the given entity from the database.
* <p>
* If the entity does no longer exist, the given instance is returned.
*
* @param entity the entity to refresh
* @param <E> the generic type of the entity
* @return a new instance of the given entity with the most current data from the database or the original entity,
* if the entity does no longer exist in the database.
*/
@CheckReturnValue
public <E extends B> E tryRefresh(E entity) {
if (entity != null) {
Optional<E> result = findEntity(entity);
if (result.isPresent()) {
return result.get();
}
}
return entity;
}
protected abstract <E extends B> Optional<E> findEntity(E entity);
/**
* Tries to fetch a fresh (updated) instance of the given entity from the database.
* <p>
* If the entity does no longer exist, an exception will be thrown.
*
* @param entity the entity to refresh
* @param <E> the generic type of the entity
* @return a new instance of the given entity with the most current data from the database.
* @throws HandledException if the entity no longer exists in the database.
*/
@CheckReturnValue
public <E extends B> E refreshOrFail(E entity) {
if (entity == null) {
return null;
}
Optional<E> result = findEntity(entity);
if (result.isPresent()) {
return result.get();
} else {
throw Exceptions.handle()
.to(Mixing.LOG)
.withSystemErrorMessage(
"Cannot refresh entity '%s' of type '%s' (entity cannot be found in the database)",
entity,
entity.getClass())
.handle();
}
}
/**
* Creates a query for the given type.
*
* @param type the type of entities to query for.
* @param <E> the generic type of entities to be returned
* @return a query used to search for entities of the given type
*/
public abstract <E extends B> Q select(Class<E> type);
/**
* Returns the filter factory which is used by this mapper.
*
* @return the filter factory used by this mapper
*/
public abstract FilterFactory<C> filters();
/**
* Provides the most efficient way of retrieving the field value of the requested entity.
* <p>
* Note that it is probably advisable to not call this method directly but rather
* {@link FieldLookupCache#lookup(BaseEntityRef, String)} which provides a cache.
*
* @param type the type of the entity
* @param id the ID of the entity
* @param field the field to resolve
* @return the field value, transformed into the appropriate type
* @throws Exception in case of an error during a lookup
*/
public abstract Value fetchField(Class<? extends B> type, Object id, Mapping field) throws Exception;
}