Skip to content

Commit 8ccc428

Browse files
committed
Adding documentation
1 parent 5090334 commit 8ccc428

File tree

3 files changed

+191
-14
lines changed

3 files changed

+191
-14
lines changed

README.md

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,163 @@
11
# JPA mapping utilities for MapStruct
2-
3-
TODO: write readme file
42

5-
## Dependencies
6-
7-
* Java >= 7
3+
A set of utilities focused on mapping JPA managed entities with MapStruct. There are different utilities for different purposes and also a all-in-one utility for maximizing ease of use.
4+
5+
## Features
6+
7+
* Domain model graph with cycles - via `CyclicGraphContext`
8+
* JPA aware mapping with update capability - via `JpaMappingContext` factory
9+
* [N+1 problem](https://stackoverflow.com/questions/97197/what-is-the-n1-select-query-issue) solution via special uninitialized collection classes, that throws exceptions if used
10+
11+
### Domain model graph with cycles
12+
13+
If you need to map a domain model with cycles in entity graph for ex.: (Pet.owner -> Person, Person.pets -> Pet) you can use a `CyclicGraphContext` as a MapStruct `@Context`
14+
15+
```java
16+
@Mapper
17+
interface PetMapper {
18+
Pet map(PetData data, @Context CyclicGraphContext context);
19+
PetData map(Pet pet, @Context CyclicGraphContext context);
20+
}
21+
```
22+
23+
### JPA aware mapping with update capability
24+
25+
If you also need support for mapping JPA managed entities and be able to update them (not create new records) there more to be done. There is provided `JpaMappingContext` with factory. It requires couple more configuration to instantiate this context.
26+
27+
`JpaMappingContext` factory requires:
28+
* Supplier of `StoringMappingContext` to handle cycles - `CyclicGraphContext` can be used here,
29+
* `Mappings` object that will provides mapping for given source and target class - mapping is information how to update existing object (managed entity) with data from source object,
30+
* `IdentifierCollector` should collect managed entity ID from source object
31+
32+
The easiest way to setup all of this is to extend `AbstractCompositeContextProvider`, implement `IdentifierCollector` and implement a set of `MappingProvider` for each type of entity. To provide implementations of `MappingProvider` you should create update methods in your MapStruct mappers.
33+
34+
All of this can be managed by some DI container like Spring or Guice.
35+
36+
**Mapping facade as Spring service:**
37+
38+
```java
39+
@Service
40+
@RequiredArgsConstructor
41+
final class MapperFacadeImpl implements MapperFacade {
42+
43+
private final PetMapper petMapper;
44+
private final MapStructContextProvider<CompositeContext> contextProvider;
45+
46+
@Override
47+
public PetJPA map(Pet pet) {
48+
return petMapper.map(pet, contextProvider.createNewContext());
49+
}
50+
51+
@Override
52+
public Pet map(PetJPA jpa) {
53+
return petMapper.map(jpa, contextProvider.createNewContext());
54+
}
55+
}
56+
```
57+
58+
**Context provider as Spring service:**
59+
60+
```java
61+
@Service
62+
@RequiredArgsConstructor
63+
final class CompositeContextProvider extends AbstractCompositeContextProvider {
64+
65+
@Getter
66+
private final JpaMappingContextFactory jpaMappingContextFactory;
67+
private final List<MappingProvider<?, ?, ?>> mappingProviders;
68+
@Getter
69+
private final IdentifierCollector identifierCollector;
70+
71+
@Override
72+
protected Iterable<MappingProvider> getMappingProviders() {
73+
return Collections.unmodifiableSet(mappingProviders);
74+
}
75+
76+
}
77+
```
78+
79+
**Example mapping provider for Pet as Spring service:**
80+
81+
```java
82+
@Service
83+
@RequiredArgsConstructor
84+
final class PetMappingProvider implements MappingProvider<Pet, PetJPA, CompositeContext> {
85+
86+
private final PetMapper petMapper;
87+
88+
@Override
89+
public Mapping<Pet, PetJPA, CompositeContext> provide() {
90+
return AbstractCompositeContextMapping.mapperFor(
91+
Pet.class, PetJPA.class,
92+
petMapper::updateFromPet
93+
);
94+
}
95+
}
96+
```
97+
98+
**Identifier collector implementation as Spring service:**
99+
100+
```java
101+
@Service
102+
final class IdentifierCollectorImpl implements IdentifierCollector {
103+
@Override
104+
public Optional<Object> getIdentifierFromSource(Object source) {
105+
if (source instanceof AbstractEntity) {
106+
AbstractEntity entity = AbstractEntity.class.cast(source);
107+
return Optional.ofNullable(
108+
entity.getReference()
109+
);
110+
}
111+
return Optional.empty();
112+
}
113+
}
114+
```
115+
116+
**HINT:** Complete working example in Spring can be seen in [coi-gov-pl/spring-clean-architecture hibernate module](https://github.com/coi-gov-pl/spring-clean-architecture/tree/develop/pets/persistence-hibernate/src/main/java/pl/gov/coi/cleanarchitecture/example/spring/pets/persistence/hibernate/mapper)
117+
118+
**HINT:** An example for Guice can be seen in this repository in test packages.
119+
120+
### N+1 problem solution via special uninitialized collection classes
121+
122+
The N+1 problem is wide known and prominent problem when dealing with JPA witch utilizes lazy loading of data. Solution to this is that developers should fetch only data that they will need (for ex.: using `JOIN FETCH` in JPQL). In many cases that is not enough. It easy to slip some loop when dealing with couple of records.
123+
124+
My solution is to detect that object is not loaded fully and provide a stub that will fail fast if data is not loaded and been tried to be used by other developer. To do that simple use `Uninitialized*` classes provided. There are `UninitializedList`, `UninitializedSet`, and `UninitializedMap`.
125+
126+
```java
127+
@Mapper
128+
interface PetMapper {
129+
// [..]
130+
default List<Pet> petJPASetToPetList(Set<PetJPA> set,
131+
@Context CompositeContext context) {
132+
if (!Hibernate.isInitialized(set)) {
133+
return new UninitializedList<>(PetJPA.class);
134+
}
135+
return set.stream()
136+
.map(j -> map(j, context))
137+
.collect(Collectors.toList());
138+
}
139+
// [..]
140+
}
141+
```
142+
143+
**Disclaimer:** In future we plan to provide an automatic solution using dynamic proxy objects.
144+
145+
## Dependencies
146+
147+
* Java >= 8
148+
* [MapStruct JDK8](https://github.com/mapstruct/mapstruct/tree/master/core-jdk8) >= 1.2.0
8149
* [EID Exceptions](https://github.com/wavesoftware/java-eid-exceptions) library
9-
10-
### Contributing
11-
150+
151+
### Contributing
152+
12153
Contributions are welcome!
13-
154+
14155
To contribute, follow the standard [git flow](http://danielkummer.github.io/git-flow-cheatsheet/) of:
15-
156+
16157
1. Fork it
17158
1. Create your feature branch (`git checkout -b feature/my-new-feature`)
18159
1. Commit your changes (`git commit -am 'Add some feature'`)
19160
1. Push to the branch (`git push origin feature/my-new-feature`)
20161
1. Create new Pull Request
21-
162+
22163
Even if you can't contribute code, if you have an idea for an improvement please open an [issue](https://github.com/wavesoftware/java-mapstruct-jpa/issues).

src/test/java/pl/wavesoftware/test/MappingTest.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pl.wavesoftware.test.entity.Pet;
2020
import pl.wavesoftware.test.jpa.PetJPA;
2121
import pl.wavesoftware.test.mapper.MapperFacade;
22+
import pl.wavesoftware.utils.mapstruct.jpa.collection.LazyInitializationException;
2223

2324
import javax.persistence.EntityManager;
2425
import java.util.ArrayList;
@@ -71,7 +72,7 @@ public void testMapFromPetToJPA() {
7172
assertThat(aliceJpa).isNotNull();
7273
assertThat(mapper).isNotNull();
7374
assertThat(aliceJpa.getId()).isEqualTo(TestRepository.ALICE_ID);
74-
assertThat(alice.getOwner()).isNotNull();
75+
assertThat(aliceJpa.getOwner()).isNotNull();
7576
assertThat(aliceJpa.getOwner().getName()).isEqualTo(TestRepository.OWNER_NAME);
7677
assertThat(aliceJpa.getOwner().getSurname()).isEqualTo("Wick");
7778
}
@@ -128,6 +129,35 @@ public void testMissingMappingForToy() {
128129
mapper.map(alice);
129130
}
130131

132+
@Test
133+
public void testMappingOnLazy() {
134+
// given
135+
Injector injector = createInjector((Module) binder ->
136+
binder.bind(EntityManager.class).toInstance(entityManager)
137+
);
138+
MapperFacade mapper = injector.getInstance(MapperFacade.class);
139+
Execution execution = testRepository.forCase(Example.LAZY);
140+
Database<PetJPA> database = execution.createJpaPetNamedAlice();
141+
PetJPA aliceJPA = database.getObject();
142+
143+
// when
144+
Pet alice = mapper.map(aliceJPA);
145+
146+
// then
147+
assertThat(alice).isNotNull();
148+
assertThat(alice.getName()).isEqualTo("Alice");
149+
thrown.expect(LazyInitializationException.class);
150+
thrown.expectMessage(
151+
"Trying to use uninitialized collection for type: List<PetJPA>. " +
152+
"You need to fetch this collection before using it, for ex. using JOIN FETCH in JPQL. " +
153+
"This exception prevents lazy loading n+1 problem."
154+
);
155+
// mapping has worked okey, but pets list of owner should be lazy loaded and should
156+
// cause error if used
157+
Optional.ofNullable(alice.getOwner())
158+
.ifPresent(o -> o.getPets().size());
159+
}
160+
131161
private void bindEntityManager(Database<?> database) {
132162
Class<?> cls = any(Class.class);
133163
when(entityManager.find(cls, anyLong()))

src/test/java/pl/wavesoftware/test/TestRepository.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.AccessLevel;
44
import lombok.RequiredArgsConstructor;
5+
import org.hibernate.collection.internal.PersistentSet;
56
import pl.wavesoftware.test.entity.AbstractEntity;
67
import pl.wavesoftware.test.entity.Owner;
78
import pl.wavesoftware.test.entity.Pet;
@@ -46,7 +47,8 @@ Execution forCase(Example example) {
4647

4748
enum Example {
4849
STANDARD,
49-
WITH_TOY
50+
WITH_TOY,
51+
LAZY
5052
}
5153

5254
interface Execution {
@@ -117,7 +119,11 @@ public Database<PetJPA> createJpaPetNamedAlice() {
117119
owner.setSurname(OWNER_SURNAME);
118120

119121
alice.setOwner(owner);
120-
owner.getPets().addAll(Arrays.asList(alice, kitie));
122+
if (example == Example.LAZY) {
123+
owner.setPets(new PersistentSet());
124+
} else {
125+
owner.getPets().addAll(Arrays.asList(alice, kitie));
126+
}
121127

122128
DatabaseImpl<PetJPA> db = new DatabaseImpl<>(alice, candidate -> {
123129
if (candidate instanceof AbstractRecord) {

0 commit comments

Comments
 (0)