What You'll Learn

If we know that the application we're going to write is only going to use a single relational database for the foreseeable future and that we expect to have the time and budget to refactor the application should a database change (say to MongoDB) was to be requested, then we can choose to use JPA in the domain layer.

In this tutorial, we are going to make that change. It is quite extensive as we're going to turn the domain objects into entities, then change the JPA repositories to use these domain classes, adjust the repository interfaces (to conform with JPA expectations) and then map the service layer.

The real problem still comes when we map from the DTOs to the domain which isn't that different to what we've experienced before.

To do this, we can move most of the code from the existing entity classes into the domain classes. Examine the domain objects in the repository.

We'd like to retain the dependency on interfaces rather than implementations so we'll tweak the CharityRepository interface so that we use save() rather than saveCharity().

To do the injection, the JPA repository needs to implement the interface.

IN CharityRepositoryJPA.java

public interface CharityRepositoryJPA extends JpaRepository<Charity, Long> , CharityRepository
//...

Since we've adapted the repository and interface to fit, most of the changes to the service layer are small.

However, the service layer does need to do some work.

In CharitysearchImpl.java

@Override
    public void saveCharity(CharityDTO aCharityDTO) {
        log.debug("Saving charity" + aCharityDTO);
        Optional<Charity> theCharity = charityRepository.findByAcronym(aCharityDTO.getAcronym());
        if (theCharity.isPresent()) {
            aCharityDTO.applyChangesTo(theCharity.get());
            charityRepository.save(theCharity.get());
        } else {
            charityRepository.save(aCharityDTO.toCharity());
        }
    }

This shows a typical pattern. Through a previous request, the service has obtained data (as entities), converted them to DTOs, and returned those to the MVC layer.

The next request comes in. It's basically telling the service layer what has happened and what data to apply when processing this event.

What has to be taken into consideration here is that the data may have changed between the client getting it and the new event being posted.

What we definitely know, is that even with OSIV (open session in view) the entities are gone.

So, we get the entity. The service then takes the DTOs and applies any required changes to the entity. If the charity doesn't exist, then we know that this is a new charity so we create a new entity (id is set to null) and save it.

Applying the changes

We put the responsibilty for applying the changes in the DTO. It's the object that knows the changes, so let's give it a charity entity to update and give it the responsibility for doing so.

The alternative would be to ask the entity to take of itself. To do, we'd consider adding a CharityValue class and make dealing with that a part of the entity's responsibilities. The service would map the DTO into the value object and then delegate to the entity.

Changing the charity is relatively straight-forward, but the trustees are harder. To do this, we need to "merge" the DTOs representing the new trustee data with the existing trustee entity data.

This is done in the service layer.

In CharityServiceImpl.java

public void updateTrusteesOfCharity(CharityDTO charityDTO, List<TrusteeDTO> trusteeDTOList) {
        Charity charity = charityRepository.findByAcronym(charityDTO.getAcronym()).get();

        Set<Trustee> trustees = charity.getTrustees();
        Set<Trustee> trusteesToRemove = new HashSet<>();

        //update the existing trustees matching on id and mark those for deletion.
        for (Trustee trustee : trustees) {
            Optional<TrusteeDTO> match = trusteeDTOList
                    .stream()
                    .filter(t -> t.getId() != null)
                    .filter(t -> t.getId().equals(trustee.getId()))
                    .findFirst();
            if (match.isPresent()) {
                trustee.setName(match.get().getName());
                trustee.setRole(match.get().getRole());
            } else {
                trusteesToRemove.add(trustee);
            }
        }

        trustees.removeAll(trusteesToRemove);


        Set<Trustee> newTrustees = trusteeDTOList
                .stream()
                .filter(t -> t.getId() == null)
                .map(t -> new Trustee(null, t.getName(), t.getRole()))
                .collect(Collectors.toSet());

        trustees.addAll(newTrustees);

        charityRepository.save(charity);
    }

The update works in three phases.

Firstly, we update the trustee data that can be matched by id. Those that do match are updated. If existing trustees can't be matched to the DTOs, then they must have been deleted, so we put those entities into a separate collection (for deletion).

Once that first past has been made, we use the stored collection to delete trustees.

Finally, we need to look at the DTOs for new data that doesn't have a trustee id. These need to be added, so we create trustee entities and add them to collection. We then save the charity.

Extra attributes on the relationship mapping

In Charity.java

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name="charity_id", nullable = false)
    private Set<Trustee> trustees;

The CascadeType defines how actions performed on a target entity should affect an associated entity.

The most common example of this would be an Order that consists of many order lines - each being an order for a product. If we delete an order, we want the order lines to be deleted, but not the products.

We've set the type to ALL which that all operations will be cascaded.

There are a number of Cascade Options but the most common effects are to save the associated entities or to delete them. Again, the use the example of the order; if we save the order, we save the order lines. If we delete the order, we delete the order lines.

If we delete an entity from the "many" side, we want the associated row to be deleted as well. e.g. if we delete an order line object from the collection of order line objects held by an order object, we want to delete the order line row as well.

When we delete the order line object from the collection, that entity becomes an orphan and so by setting `orphanRemoval=true, we tell JPA to delete it from the database as well.

More on Orphan Removal.

The nullable=false attribute on @JoinColumn is only useful if Hibernate is creating the DDL. If this is the case, nullable tells JPA/Hibernate to make the database column NOT NULL.

In this tutorial, we've taken the significant decision to use JPA directly within our domain.

This reduces the number of hand-offs and should increase our productivity, but it ties us into our architecture.

It is (to paraphrase Grady Booch) a clear example of an architectual decision because the cost to undo that decision would be high.

The tutorial also shows that JPA has some complications that need to be thought about when setting it up. It is also worth considering though that this work, once done, does allow the developer to work directly with objects and with some consistency in their approach, which may lead to higher overall productivity in the long term.

On the flip-side, JPA may prevent the developer from using database-specific features. Again, this would be a trade-off and it is useful to make it clear which components have made these trade-offs and which have not.