Skip to content

Commit 5fb453f

Browse files
authored
Documentation enhancements (#16)
* Updated documentation - Added more information around policies and ensured the documentation of "forChildrenOf" were correctly renamed to "forThisOrChildrenOf" - Added methods to the AeroMapper to get the policies explicitly if desired. - Documented the ability to pass 2 parameters to setters of properties or keys, and added a unit test to the same - Corrected a bug in the NonJavaMapperApplication unit test. - Removed appropriate TODOs - Added sections on batch loading, custom type conversions, etc - Removed some obsolete TODOs - Added a couple of images to help explain concepts. - Extra logging to try to resolve test failure - Fixed simpleDateFormat for timezone/24 hour format. - Moved the TODO list out of the documentaion
1 parent c0c72b4 commit 5fb453f

File tree

8 files changed

+422
-28
lines changed

8 files changed

+422
-28
lines changed

README.md

Lines changed: 184 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ If multiple configuration files are used and the same class is defined in multip
200200
After the specified policy, there are 3 possible options:
201201
202202
- `forAll()`: The passed policy is used for all classes. This is similar to setting the defaultReadPolicy on the IAerospikeClient but allows it to be set after the client is created.
203-
- `forChildrenOf(Class<?> class)`: The passed policy is used for the passed class and all subclasses of the passed class.
203+
- `forThisOrChildrenOf(Class<?> class)`: The passed policy is used for the passed class and all subclasses of the passed class.
204204
- `forClasses(Class<?> ... classes)`: The passed policy is used for the passed class(es), but no subclasses.
205205
206206
It is entirely possible that a class falls into more than one category, in which case the most specific policy is used. If no policy is specified, the defaultReadPolicy passed to the IAerospikeClient is used. For example, if there are classes A, B, C with C being a subclass of B, a definition could be for example:
@@ -210,19 +210,34 @@ Policy readPolicy1, readPolicy2, readPolicy3;
210210
// ... code to set up the policies goes here...
211211
AeroMapper.Builder(client)
212212
.withReadPolicy(readPolicy1).forAll()
213-
.withReadPolicy(readPolicy2).forChildrenOf(B.class)
213+
.withReadPolicy(readPolicy2).forThisOrChildrenOf(B.class)
214214
.withReadPolicy(readPolicy3).forClasses(C.class)
215215
.build();
216216
```
217217
218-
In this case the `forAll()` would apply to A,B,C, the `forChildrenOf` would apply to B,C and `forClasses` would apply to C. So the policies used for each class would be:
218+
In this case the `forAll()` would apply to A,B,C, the `forThisOrChildrenOf` would apply to B,C and `forClasses` would apply to C. So the policies used for each class would be:
219219
220220
- A: `readPolicy1`
221221
- B: `readPolicy2`
222222
- C: `readPolicy3`
223223
224224
Note that each operation can also optionally take a policy if it is desired to change any of the policy settings on the fly. The explicitly provided policy will override any other settings, such as `durableDelete` on the `@AerospikeRecord`
225-
225+
226+
if it is desired to change one part of a policy but keep the rest as the defaults set up with these policies, the appropriate policy can be read with `getReadPolicy`, `getWritePolicy`, `getBatchPolicy`, `getScanPolicy` and `getQueryPolicy` methods on the AeroMapper. For example, if we needed a policy which was preiously set up on a Customer class but needed to change the `durableDelete` property, we could do
227+
228+
```java
229+
WritePolicy writePolicy = mapper.getWritePolicy(Customer.class);
230+
writePolicy.durableDelete = true;
231+
mapper.delete(writePolicy, myCustomer);
232+
```
233+
234+
In summary, the policy which will be used for a call are: (lower number is a higher priority)
235+
236+
1. Policy passed as a parameter
237+
2. Policy passed to `forClasses` method
238+
3. Policy passed to `forThisOrChildrenOf` method
239+
4. Policy passed to `forAll` method
240+
5. AerospikeClient.getXxxxPolicyDefault
226241
227242
---
228243
@@ -456,6 +471,131 @@ public int getCraziness() {
456471

457472
This will create a bin in the database with the name "bob".
458473

474+
It is possible for the setter to take an additional parameter too, providing this additional parameter is either a `Key` or `Value` object. This will be the key of the last object being loaded.
475+
476+
So, for example, if we have an A object which embeds a B, when the setter for B is called the second parameter will represent A's key:
477+
478+
```java
479+
@AerospikeRecord(namespace = "test", set = "A", mapAll = false)
480+
public class A {
481+
@AerospikeBin
482+
private String key;
483+
private String value1;
484+
private long value2;
485+
486+
@AerospikeGetter(name = "v1")
487+
public String getValue1() {
488+
return value1;
489+
}
490+
@AerospikeSetter(name = "v1")
491+
public void setValue1(String value1, Value owningKey) {
492+
// owningKey.getObject() will be a String of "B-1"
493+
this.value1 = value1;
494+
}
495+
496+
@AerospikeGetter(name = "v2")
497+
public long getValue2() {
498+
return value2;
499+
}
500+
501+
@AerospikeSetter(name = "v2")
502+
public void setValue2(long value2, Key key) {
503+
// Key will have namespace="test", setName = "B", key.userKey.getObject() = "B-1"
504+
this.value2 = value2;
505+
}
506+
}
507+
508+
@AerospikeRecord(namespace = "test", set = "B")
509+
public class B {
510+
@AerospikeKey
511+
private String key;
512+
@AerospikeEmbed
513+
private A a;
514+
}
515+
516+
@Test
517+
public void test() {
518+
A a = new A();
519+
a.key = "A-1";
520+
a.value1 = "value1";
521+
a.value2 = 1000;
522+
523+
B b = new B();
524+
b.key = "B-1";
525+
b.a = a;
526+
527+
AeroMapper mapper = new AeroMapper.Builder(client).build();
528+
mapper.save(b);
529+
B b2 = mapper.read(B.class, b.key);
530+
531+
}
532+
```
533+
534+
This can be useful in situations where the full key does not need to be stored in subordinate parts of the record. Consider a time-series use case where transactions are stored in a transaction container. The transactions for a single day might be grouped into a single transaction container, and the time of the transaction in microseconds may be the primary key of the transaction. If we model this with the transactions in the transaction container, the key for the transaction record could simply be the number of microseconds since the start of the day, as the microseconds representing the start of the day would be contained in the day number used as the transaction container key.
535+
536+
Since this information is redundant, it could be stripped out, shortening the length of the transaction key and hence saving storage space. However, when we wish to rebuild the transaction, we need the key of the transaction container to be able to derive the microseconds of the key to the start of the day to reform the appropriate transaction key.
537+
538+
----
539+
540+
## Default Mappings of Java Data type
541+
Here are how standard Java types are mapped to Aerospike types:
542+
| Java Type | Aerospike Type |
543+
| --- | --- |
544+
| byte | integral numeric |
545+
| char | integral numeric |
546+
| short | integral numeric |
547+
| int | integral numeric |
548+
| long | integral numeric |
549+
| boolean | integral numeric |
550+
| Byte | integral numeric |
551+
| Character | integral numeric |
552+
| Short | integral numeric |
553+
| Integer | integral numeric |
554+
| Long | integral numeric |
555+
| Boolean | integral numeric |
556+
| float | double numeric |
557+
| double | double numeric |
558+
| Float | double numeric |
559+
| Double | double numeric |
560+
| java.util.Date | integral numeric |
561+
| java.time.Instant | integral numeric |
562+
| String | String |
563+
| byte[] | BLOB |
564+
| enums | String |
565+
| Arrays (int[], String[], Customer[], etc) | List |
566+
| List<?> | List or Map |
567+
| Map<?,?> | Map |
568+
| Object Reference (@AerospikeRecord) | List or Map |
569+
570+
These types are built into the converter. However, if you wish to change them, you can use a (Custom Object Converter)]custom-object-converter]. For example, if you want Dates stored in the database as a string, you could do:
571+
572+
```java
573+
public static class DateConverter {
574+
private static final ThreadLocal<SimpleDateFormat> dateFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS zzzZ"));
575+
@ToAerospike
576+
public String toAerospike(Date date) {
577+
if (date == null) {
578+
return null;
579+
}
580+
return dateFormatter.get().format(date);
581+
}
582+
583+
@FromAerospike
584+
public Date fromAerospike(String dateStr) throws ParseException {
585+
if (dateStr == null) {
586+
return null;
587+
}
588+
return dateFormatter.get().parse(dateStr);
589+
}
590+
}
591+
592+
AeroMapper convertingMapper = new AeroMapper.Builder(client).addConverter(new DateConverter()).build();
593+
```
594+
595+
(Note that SimpleDateFormat is not thread-safe, and hence the use of the ThreadLocal variable)
596+
597+
This would affect all dates. If you wanted to affect the format of some dates, create a sub-class Date and have the converter change that to the String format.
598+
459599
----
460600
461601
## References to other objects
@@ -707,6 +847,30 @@ Note that storing the digest as the referencing key is not compatible with lazy
707847

708848
will throw an exception at runtime.
709849

850+
#### Batch Loading
851+
852+
Note that when objects are stored by non-lazy references, all dependent children objects will be loaded by batch loading. For example, assume there is a complex object graph like:
853+
854+
![Object Diagram](/images/complexObjectGraph.png)
855+
856+
Note that some of the objects are embedded and some are references.
857+
858+
If we then instantiate a complex object graph like:
859+
860+
![Object Graph](/images/objectInstantiation.png)
861+
862+
Here you can see the Customer has a lot of dependent objects, where the white objects are being loaded by reference and the grey objects are being embedded into the parent. When the Customer is loaded the entire object graph is loaded. Looking at the calls that are performed to the database, we see:
863+
864+
```
865+
Get: [test:customer:cust1:818d8a436587c36aef4da99d28eaf17e3ce3a0e1] took 0.211ms, record found
866+
Batch: [4/4 keys] took 0.258ms
867+
Batch: [6/6 keys] took 0.262ms
868+
Batch: [2/2 keys] took 0.205ms
869+
```
870+
871+
The first call (the `get`) is for the Customer object, the first batch of 4 is for the Cusomter's 4 accounts (Checking, Savings, Loan, Portfolio), the second batch of 6 items is for the 2 checkbooks and 4 security properties, and the last batch of 2 items is for the 2 branches. The AeroMapper will load all dependent objects it can in one hit, even if they're of different classes. This includes elements within LIsts, Arrays and Maps as well as straight dependent objects. This can make loading complex object graphs very efficient.
872+
873+
710874
### Aggregating by Embedding
711875
The other way object relationships can be modeled is by embedding the child object(s) inside the parent object. For example, in some banking systems, Accounts are based off Products. The Products are typically versioned but can have changes made to them by banking officers. Hence the product is effectively specific to a particular account, even though it is derived from a global product. In this case, it makes sense to encapsulate the product into the account object.
712876

@@ -1407,8 +1571,23 @@ Note that if an object is mapped to the actual type (eg Account to Account) then
14071571

14081572
For this reason, it is strongly recommended that all attributes use a parameterized type, eg `List<Account>` rather than `List`
14091573

1410-
It should be noted that the use of subclasses
1574+
It should be noted that the use of subclasses can have a minor degradation on performance. When the declared type is the same as the instantiated type, the Java Object Mapper has already computed the optimal way of accessing that information. If it encounters a sub-class at runtime (i.e. the instantiated type is not the same as the declared type), it must then work out how to store the passed sub-class. The sub-class information is also typically cached so the performance hit should not be significant, but it is there.
1575+
1576+
By the same token, it is always better to use Java generics in collection types to give the Java Object Mapper hints about how to store the data in Aerospike so it can optimize its internal processes.
1577+
1578+
For example, say we need a list of Customers as a field on a class. We could declare this as:
1579+
1580+
```java
1581+
public List<Customer> customers;
1582+
```
1583+
1584+
or
14111585

1586+
```java
1587+
public List customers;
1588+
```
1589+
1590+
The former is considered better style in Java and also provides the Java Object Mapper with information about the elements in the list, so it will optimize its workings to know how to store a list of Customers. The latter gives it no type information so it must derive the type -- and hence how to map it to Aerospike -- for every element in this list. This can have a noticeable performance impact for large lists, as well as consuming more database space (as it must store the runtime type of each element in the list in addition to the data).
14121591

14131592

14141593
----
@@ -1895,18 +2074,3 @@ public <T> T convertToObject(Class<T> clazz, Record record);
18952074

18962075
Note: At the moment not all CDT operations are supported, and if the underlying CDTs are of the wrong type, a different API call may be used. For example, if you invoke `getByKeyRange` on items represented in the database as a list, `getByValueRange` is invoked instead as a list has no key.
18972076

1898-
1899-
----
1900-
1901-
## To finish
1902-
- Add interface to adaptiveMap, including changing EmbedType
1903-
- Document all parameters to annotations and examples of types
1904-
- Document enums, dates, instants.
1905-
- Document methods with 2 parameters for keys and setters, the second one either a Key or a Value
1906-
- Document subclasses and the mapping to tables + references stored as lists
1907-
- Batch load of child items on Maps and References. Ensure testing of non-parameterized classes too.
1908-
- Document batch loading
1909-
- Ensure batch loading option exists in AerospikeReference Configuration
1910-
- handle object graph circularities (A->B->C). Be careful of: A->B(Lazy), A->C->B: B should end up fully hydrated in both instances, not lazy in both instances
1911-
- Consider the items on virtual list which return a list to be able to return a map as well (ELEMENT_LIST, ELEMENT_MAP)
1912-
- Test a constructor which requires a sub-object. For example, Account has a Property, Property has an Address. All 3 a referenced objects. Constructor for Property requires Address

images/complexObjectGraph.png

97.6 KB
Loading

images/objectInstantiation.png

109 KB
Loading

src/main/java/com/aerospike/mapper/tools/AeroMapper.java

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,81 @@ public <T> Map<String, Object> convertToMap(@NotNull T instance) {
640640
return entry.getMap(instance, false);
641641
}
642642

643+
/**
644+
* Return the read policy to be used for the passed class. This is a convenience method only and should rarely be needed
645+
* @param clazz - the class to return the read policy for.
646+
* @return - the appropriate read policy. If none is set, the client's readPolicyDefault is returned.
647+
*/
648+
public Policy getReadPolicy(Class<?> clazz) {
649+
ClassCacheEntry<?> entry = ClassCache.getInstance().loadClass(clazz, this);
650+
if (entry == null) {
651+
return this.mClient.getReadPolicyDefault();
652+
}
653+
else {
654+
return entry.getReadPolicy();
655+
}
656+
}
657+
658+
/**
659+
* Return the write policy to be used for the passed class. This is a convenience method only and should rarely be needed
660+
* @param clazz - the class to return the write policy for.
661+
* @return - the appropriate write policy. If none is set, the client's writePolicyDefault is returned.
662+
*/
663+
public WritePolicy getWritePolicy(Class<?> clazz) {
664+
ClassCacheEntry<?> entry = ClassCache.getInstance().loadClass(clazz, this);
665+
if (entry == null) {
666+
return this.mClient.getWritePolicyDefault();
667+
}
668+
else {
669+
return entry.getWritePolicy();
670+
}
671+
}
672+
673+
/**
674+
* Return the batch policy to be used for the passed class. This is a convenience method only and should rarely be needed
675+
* @param clazz - the class to return the batch policy for.
676+
* @return - the appropriate batch policy. If none is set, the client's batchPolicyDefault is returned.
677+
*/
678+
public BatchPolicy getBatchPolicy(Class<?> clazz) {
679+
ClassCacheEntry<?> entry = ClassCache.getInstance().loadClass(clazz, this);
680+
if (entry == null) {
681+
return this.mClient.getBatchPolicyDefault();
682+
}
683+
else {
684+
return entry.getBatchPolicy();
685+
}
686+
}
687+
688+
/**
689+
* Return the scan policy to be used for the passed class. This is a convenience method only and should rarely be needed
690+
* @param clazz - the class to return the scan policy for.
691+
* @return - the appropriate scan policy. If none is set, the client's scanPolicyDefault is returned.
692+
*/
693+
public ScanPolicy getScanPolicy(Class<?> clazz) {
694+
ClassCacheEntry<?> entry = ClassCache.getInstance().loadClass(clazz, this);
695+
if (entry == null) {
696+
return this.mClient.getScanPolicyDefault();
697+
}
698+
else {
699+
return entry.getScanPolicy();
700+
}
701+
}
702+
703+
/**
704+
* Return the query policy to be used for the passed class. This is a convenience method only and should rarely be needed
705+
* @param clazz - the class to return the query policy for.
706+
* @return - the appropriate query policy. If none is set, the client's queryPolicyDefault is returned.
707+
*/
708+
public Policy getQueryPolicy(Class<?> clazz) {
709+
ClassCacheEntry<?> entry = ClassCache.getInstance().loadClass(clazz, this);
710+
if (entry == null) {
711+
return this.mClient.getQueryPolicyDefault();
712+
}
713+
else {
714+
return entry.getQueryPolicy();
715+
}
716+
}
717+
643718
/**
644719
* If an object refers to other objects (eg A has a list of B via references), then reading the object will populate the
645720
* ids. If configured to do so, these objects can be loaded via a batch load and populated back into the references which
@@ -660,7 +735,7 @@ void resolveDependencies(ClassCacheEntry<?> parentEntity) {
660735
BatchPolicy batchPolicy = parentEntity == null ? mClient.getBatchPolicyDefault() : parentEntity.getBatchPolicy();
661736
BatchPolicy batchPolicyClone = new BatchPolicy(batchPolicy);
662737

663-
while (deferredObjects != null && !deferredObjects.isEmpty()) {
738+
while (!deferredObjects.isEmpty()) {
664739
int size = deferredObjects.size();
665740

666741
ClassCacheEntry<?>[] classCaches = new ClassCacheEntry<?>[size];
@@ -707,6 +782,4 @@ void resolveDependencies(ClassCacheEntry<?> parentEntity) {
707782
deferredObjects = DeferredObjectLoader.getAndClear();
708783
}
709784
}
710-
711-
712785
}
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
package com.aerospike.mapper.tools;
22

3+
import java.util.ArrayDeque;
4+
import java.util.Deque;
5+
36
import com.aerospike.client.Key;
47

8+
/**
9+
* Save the keys. Note that this is effectively a stack of keys, as A can load B which can load C, and C needs B's key, not A's.
10+
* @author timfaulkes
11+
*/
512
public class ThreadLocalKeySaver {
6-
private static ThreadLocal<Key> threadLocalKey = new ThreadLocal<>();
13+
private static final ThreadLocal<Deque<Key>> threadLocalKeys = ThreadLocal.withInitial(ArrayDeque::new);
714

815
public static void save(Key key) {
9-
threadLocalKey.set(key);
16+
threadLocalKeys.get().addLast(key);
1017
}
1118

1219
public static void clear() {
13-
threadLocalKey.set(null);
20+
threadLocalKeys.get().removeLast();
1421
}
1522

1623
public static Key get() {
17-
return threadLocalKey.get();
24+
return threadLocalKeys.get().getLast();
1825
}
1926
}

0 commit comments

Comments
 (0)