JPA mapping utilities for MapStruct

A set of utilities focused on mapping JPA managed entities with MapStruct

License

License

Categories

Categories

MapStruct General Purpose Libraries Bean Mapping
GroupId

GroupId

pl.wavesoftware.utils
ArtifactId

ArtifactId

mapstruct-jpa
Last Version

Last Version

0.1.0
Release Date

Release Date

Type

Type

jar
Description

Description

JPA mapping utilities for MapStruct
A set of utilities focused on mapping JPA managed entities with MapStruct
Project URL

Project URL

https://github.com/wavesoftware/java-mapstruct-jpa
Project Organization

Project Organization

Wave Software
Source Code Management

Source Code Management

https://github.com/wavesoftware/java-mapstruct-jpa

Download mapstruct-jpa

How to add to project

<!-- https://jarcasting.com/artifacts/pl.wavesoftware.utils/mapstruct-jpa/ -->
<dependency>
    <groupId>pl.wavesoftware.utils</groupId>
    <artifactId>mapstruct-jpa</artifactId>
    <version>0.1.0</version>
</dependency>
// https://jarcasting.com/artifacts/pl.wavesoftware.utils/mapstruct-jpa/
implementation 'pl.wavesoftware.utils:mapstruct-jpa:0.1.0'
// https://jarcasting.com/artifacts/pl.wavesoftware.utils/mapstruct-jpa/
implementation ("pl.wavesoftware.utils:mapstruct-jpa:0.1.0")
'pl.wavesoftware.utils:mapstruct-jpa:jar:0.1.0'
<dependency org="pl.wavesoftware.utils" name="mapstruct-jpa" rev="0.1.0">
  <artifact name="mapstruct-jpa" type="jar" />
</dependency>
@Grapes(
@Grab(group='pl.wavesoftware.utils', module='mapstruct-jpa', version='0.1.0')
)
libraryDependencies += "pl.wavesoftware.utils" % "mapstruct-jpa" % "0.1.0"
[pl.wavesoftware.utils/mapstruct-jpa "0.1.0"]

Dependencies

compile (1)

Group / Artifact Type Version
pl.wavesoftware : eid-exceptions jar 1.2.0

provided (4)

Group / Artifact Type Version
javax.persistence : javax.persistence-api jar 2.2
org.projectlombok : lombok Optional jar 1.16.20
org.mapstruct : mapstruct-jdk8 jar [1.2.0.Final,2.0.0)
org.mapstruct : mapstruct-processor Optional jar [1.2.0.Final,2.0.0)

test (5)

Group / Artifact Type Version
junit : junit jar 4.12
org.assertj : assertj-core jar 3.9.1
org.mockito : mockito-core jar 2.18.3
com.google.inject : guice jar 4.2.0
org.hibernate : hibernate-core jar 5.2.17.Final

Project Modules

There are no modules declared in this project.

JPA mapping utilities for MapStruct

Build Status Quality Gate Coverage Status Maven Central

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.

Features

  • Domain model graph with cycles - via CyclicGraphContext
  • JPA aware mapping with update capability - via JpaMappingContext factory
  • N+1 problem solution via special uninitialized collection classes, that throws exceptions if used

Domain model graph with cycles

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

@Mapper
interface PetMapper {
  Pet map(PetData data, @Context CyclicGraphContext context);
  PetData map(Pet pet, @Context CyclicGraphContext context);
}

JPA aware mapping with update capability

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.

JpaMappingContext factory requires:

  • Supplier of StoringMappingContext to handle cycles - CyclicGraphContext can be used here,
  • 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,
  • IdentifierCollector should collect managed entity ID from source object

The easiest way to setup all of this is to extend AbstractJpaContextProvider, 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. It utilize CompositeContext which can incorporate any number of contexts as a composite.

All of this can be managed by some DI container like Spring or Guice.

Mapping facade as Spring service:

@Service
@RequiredArgsConstructor
final class MapperFacadeImpl implements MapperFacade {

  private final PetMapper petMapper;
  private final MapStructContextProvider<CompositeContext> contextProvider;

  @Override
  public PetJPA map(Pet pet) {
    return petMapper.map(pet, contextProvider.createNewContext());
  }

  @Override
  public Pet map(PetJPA jpa) {
    return petMapper.map(jpa, contextProvider.createNewContext());
  }
}

Context provider as Spring service:

@Service
@RequiredArgsConstructor
final class CompositeContextProvider extends AbstractCompositeContextProvider {

  @Getter
  private final JpaMappingContextFactory jpaMappingContextFactory;
  private final List<MappingProvider<?, ?, ?>> mappingProviders;
  @Getter
  private final IdentifierCollector identifierCollector;

  @Override
  protected Iterable<MappingProvider> getMappingProviders() {
    return Collections.unmodifiableSet(mappingProviders);
  }

}

Example mapping provider for Pet as Spring service:

@Service
@RequiredArgsConstructor
final class PetMappingProvider implements MappingProvider<Pet, PetJPA, CompositeContext> {

  private final PetMapper petMapper;

  @Override
  public Mapping<Pet, PetJPA, CompositeContext> provide() {
    return AbstractCompositeContextMapping.mapperFor(
      Pet.class, PetJPA.class,
      petMapper::updateFromPet
    );
  }
}

Identifier collector implementation as Spring service:

@Service
final class IdentifierCollectorImpl implements IdentifierCollector {
  @Override
  public Optional<Object> getIdentifierFromSource(Object source) {
    if (source instanceof AbstractEntity) {
      AbstractEntity entity = AbstractEntity.class.cast(source);
      return Optional.ofNullable(
        entity.getReference()
      );
    }
    return Optional.empty();
  }
}

HINT: Complete working example in Spring can be seen in coi-gov-pl/spring-clean-architecture hibernate module

HINT: An example for Guice can be seen in this repository in test packages.

N+1 problem solution via special uninitialized collection classes

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.

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.

@Mapper
interface PetMapper {
  // [..]
  default List<Pet> petJPASetToPetList(Set<PetJPA> set,
                                       @Context CompositeContext context) {
    if (!Hibernate.isInitialized(set)) {
      return new UninitializedList<>(PetJPA.class);
    }
    return set.stream()
      .map(j -> map(j, context))
      .collect(Collectors.toList());
  }
  // [..]
}

Disclaimer: In future we plan to provide an automatic solution using dynamic proxy objects.

Dependencies

Contributing

Contributions are welcome!

To contribute, follow the standard git flow of:

  1. Fork it
  2. Create your feature branch (git checkout -b feature/my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin feature/my-new-feature)
  5. Create new Pull Request

Even if you can't contribute code, if you have an idea for an improvement please open an issue.

pl.wavesoftware.utils

Wave Software

Versions

Version
0.1.0