Thanks for the example. This makes it very simple for me to help you
The reason for what you are seeing is that House#tenants
is the owning side of the relationship, which means that data on that side will determine the contents of the relationship.
So if you merge a House
with empty tenants
, then you’ll get all tenants
removed.
House#appliances
is the inverse or non-owning side, which is irrelevant for persistence of the relationship. Only by changing Appliance#house
you can alter the relationship. It is important to keep both sides of the relationship in sync, but if you know what you are doing (caching, persistence context operations), you can also skip maintaining the inverse/non-owning side, since maintaining that might require initialization which is potentially expensive.
The basic question is this: how can I programatically, at run time, tell it to either 1) Save the graph but break all references from existing objects to other objects in the database that are not included in the request graph
I guess your problem is the removal of “orphaned” elements from the inverse/non-owned side e.g. Town#houses
and House#tenants
? If so, then I fear there is no easy answer. To unlink such relationships, you will have to load the relationships, set the owning side to e.g. null
or remove the entity and remove the element from the collection.
- How can I tell it to merge with whatever is in the database, breaking no references at all – including the many-to-many one between houses and tenants?
You can load the data first and apply the changes to the entity graph relationship by relationship in this case e.g.
Town existingTown = entityManager.find(Town.class, requestTown.getId());
if ( !requestTown.getHouses().isEmpty() ) {
for (House h : existingTown.getHouses()) {
if ( !requestTown.getHouses().contains(h)) ) {
h.setTown(null); // or entityManager.remove(h);
}
}
existingTown.getHouses().addAll(requestTown.getHouses());
}
...
This can become really cumbersome, but there is no easy way to handle this otherwise when you want to manage an unowned collection. If you make the relationship unidirectional instead, i.e. remove House#town
and map Town#houses
with @JoinColumn
, then you could handle all of this through the collection instead, but lose the querying capability of House
by town
.
If you are willing to use DTOs instead, I think this is a perfect use case for Blaze-Persistence Entity Views, which handles all of this transparently for you. Relationship ownership doesn’t matter to Entity Views with respect to updatability. It takes care of the nifty details to make this work fast.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
@EntityView(Town.class)
@UpdatableEntityView
public interface TownDto {
@IdMapping
String getName();
@UpdatableMapping(orphanRemoval = true)
Set<HouseDto> getHouses();
@EntityView(House.class)
@UpdatableEntityView
@CreatableEntityView
interface HouseDto {
@IdMapping
String getName();
void setHouse(String name);
@UpdatableMapping(orphanRemoval = true)
Set<TenantDto> getTenants();
@UpdatableMapping(orphanRemoval = true)
Set<ApplianceDto> getAppliances();
}
@EntityView(Tenant.class)
interface TenantDto {
@IdMapping
String getAmznAlias();
}
@EntityView(House.class)
@UpdatableEntityView
@CreatableEntityView
interface ApplianceDto {
@IdMapping
String getId();
void setId(String id);
ApplianceType getType();
void setType(ApplianceType type);
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
TownDto a = entityViewManager.find(entityManager, TownDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: Blaze Persistence - Entity View Module
Page<TownDto> findAll(Pageable pageable);
TownDto save(TownDto town);
The best part is, it will only fetch the state that is actually necessary!