I have seen various post describing that JPA EntityGraph allows to choose the graph at run time. And I am not entirely clear what this refers to.
Out of good faith and respect I would like to take this helpful article for reference: https://www.baeldung.com/jpa-entity-graph. (Most of JPA users might have gone through it already.)
The article quotes -
EntityGraph allows grouping the related persistence fields which we
want to retrieve and lets us choose the graph type at runtime.
and again solidifies above statement in conclusion section.
In this article, we've explored using the JPA Entity Graph to
dynamically fetch an Entity and its associations.
The decision is made at runtime in which we choose to load or not the
related association.
As we see in the article (5.1) - EntityGraphs are defined as below using Annotations-
5.1. Defining an Entity Graph with Annotations
#NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
#NamedAttributeNode("subject"),
#NamedAttributeNode("user"),
#NamedAttributeNode("comments"),
}
)
#Entity
public class Post {
#OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
The #NameEntityGraph annotation is defined at compile time and I don't see anything runtime or dynamic here.
But in 5.2 - entity graphs are defined using api or programmatically -
5.2. Defining an Entity Graph with the JPA API
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");
In 5.2 approach, I see nodes can be chosen dynamically using some logic. So is this approach is what is refered to "dynamically fetch" and "runtime based".
Or am i missing something and do i have more to understand.
Further the approaches given in 6. Using the Entity Graph
ex:
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
are all programmatic and so can be changed during runtime i.e they can be said as dynamic.
But one approach missed in above article, but mentioned here - https://www.baeldung.com/spring-data-jpa-named-entity-graphs, as below, does not seem to fit in to dynamic criteria.
public interface ItemRepository extends JpaRepository<Item, Long> {
#EntityGraph(value = "Item.characteristics")
Item findByName(String name);
}
So does the dynamic approach just refer to 5.2 style or it implies even 5.1 style too.
You can't use dynamic entity graphs with spring-data, because JpaRepository doesn't have methods to pass entity graphs like
Optional<T> findById(ID id, EntityGraph entityGraph);
Using custom JPA repository
You can use raw JPA for that, by creating a custom repository and using entity graphs with EntityManager.
Using spring-data-jpa-entity-graph
There is a more convenient approach by using library
spring-data-jpa-entity-graph.
It allows to use JPA repository methods like findById() or findByName() with dynamic entity graphs.
I prefer to use it with this helper class
public abstract class EntityGraphBuilder<T> {
private List<String> result = new ArrayList<>();
protected T self;
public T add(String path) {
result.add(path);
return self;
}
public DynamicEntityGraph build() {
return new DynamicEntityGraph(EntityGraphType.FETCH, result);
}
}
Each entity has its own GraphBuilder
#Entity
public class OrderEntity {
#Id
private Long id;
#Column
private name;
#ManyToOne(fetch = FetchType.LAZY)
private OrderRequestEntity orderRequest;
#ManyToOne(fetch = FetchType.LAZY)
private ProviderEntity provider;
public static GraphBuilder graph() {
return new GraphBuilder();
}
public static class GraphBuilder extends EntityGraphBuilder<GraphBuilder> {
private GraphBuilder() {
self = this;
}
public GraphBuilder orderRequest() {
return add("orderRequest");
}
public GraphBuilder provider() {
return add("provider");
}
}
}
Repository uses EntityGraphJpaRepository from spring-data-jpa-entity-graph library
#Repository
public interface OrdersRepository extends EntityGraphJpaRepository<OrderEntity, Long> {
OrderEntity findByName(String name, EntityGraph entityGraph);
}
You can use derived query methods like findByName() with dynamic entity graphs too.
Example of using findById() method, the same approach can be applied to findByName()
OrdersRepository ordersRepository;
Long orderId = 1L;
OrderEntity order = ordersRepository.findById(
orderId,
OrderEntity.graph().orderRequest().provider().build()
).orElseThrow(
() -> new ServiceException("Can't find orderId=" + orderId)
);
In the Baeldung article, Section 5 is only about various ways to define a graph with not much emphasis on the dynamic/non-dynamic nature of the definition itself.
In Section 5.1 the definition of the graph is static but this section is only about demonstrating how to define a graph which then will be used in typical cases when building the graph dynamically is not really necessary. This section shows an alternative to the older way of building HQL / JPA-QL queries with JOIN FETCH sections.
#NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
#NamedAttributeNode("subject"),
#NamedAttributeNode("user"),
#NamedAttributeNode("comments"),
}
)
#Entity
public class Post {
#OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
Then, Section 6 tells you how to use the entity graphs defined earlier in various ways.
// Getting the "statically" defined graph (from annotation)
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
// Then using the graph
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
Naturally, you can swap this first line to the fully dynamically built graph demonstrated in Section 5.2:
// Building the graph dynamically
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");
// Then using the graph
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
In both cases you supply an EntityGraph object to the query.
Related
I am currently working on a Spring Boot project and I would like to speed up process of writing the service/data layer boilerplate code (one service and one repository (CrudRepository) for every entity, every one having mostly the same methods).
As of now I am using TABLE_PER_CLASS inheritance in several entities (e.g.: Warehouse and Office are subclasses of Location (an abstract class defining common attributes for all locations).
I would like to define 1 repository and 1 service to manage both Location and its subtypes so I can do something like in my control layer:
#Autowired
LocationsService locationsService;
Warehouse cityWarehouse = new Warehouse();
Office centralOffice = new Office();
locationsService.addNewLocation(cityWarehouse);
locationsService.addNewLocation(centralOffice);
I know I can just use method overloading but I would really like to avoid repeating the same code in situations like this one.
I've also tried using parametric polymorphism:
#Service
public class LocationsService {
#Autowired
LocationsRepository locationsRepository;
public void addNewLocation(Location location) {
locationsRepository.save(location);
}
}
Unfortunately this won't work as Spring can't tell if I want to save a Location or a Warehouse object:
nested exception is org.springframework.orm.jpa.JpaObjectRetrievalFailureException:
Unable to find com.test.springboot.entities.locations.Location with id 55db6993-8a58-4e3a-a6ab-d60d93ab6182; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.test.springboot.entities.locations.Location with id 55db6993-8a58-4e3a-a6ab-d60d93ab6182
I need to use concrete Location objects so using #MappedSuperclass is not an option.
Is there something I am missing? Is it even posible to achieve what I want?
Please note that I am fairly new to Spring Boot so maybe there's something obvious I don't know about yet.
I got it working thanks to some of the comments and after briefly reading the JPA specification.
Because I wanted to use my superclasses as entities I ended up using SINGLE_TABLE inheritance.
For example, this is the Location entity:
#Entity
#Data
#Accessors(chain = true)
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "location_type")
public class Location {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// Skipped
}
The key here is to use #DiscriminatorColumn with SINGLE_TABLE inheritance in the parent class and add #DiscriminatorValue in the correspondent subclasses:
#Entity
#Data #Accessors(chain = true)
#DiscriminatorValue("warehouse_location")
public class Warehouse extends Location {
#JoinColumn(name = "INTERNAL_ROUTE_ID")
#OneToOne(orphanRemoval = true)
private Route internalRoute;
#JoinColumn(name = "EXTERNAL_WAREHOUSE_ID")
#OneToMany(orphanRemoval = true, fetch = FetchType.EAGER)
private List<Warehouse> externalWarehouses;
}
This way, I can define LocationsRepository as:
public interface LocationsRepository extends CrudRepository<Location, Long> {
Warehouse findByCityIgnoreCase(String city);
}
Also note that subclass-specific methods can be defined here as long as its return type is explicitly specified (otherwise the method would return ALL Locations, not just the Warehouses).
Finally, in the service layer I can make the relevant methods return any entity just by downcasting the result of the repository call into the appropriate subclass
I have a development project using Spring Data JPA and MapStruct to map between Entities and DTOs. Last week I decided it was time to address the FetchType.EAGER vs LAZY issue I have postponed for some time. I choose to use #NamedEntityGraph and #EntityGraph to load properties when needed. However I am stuck with this LazyInitializationExeption problem when doing the mapping from entity to dto. I think I know where this happens but I do not know how to get passed it.
The code
#NamedEntityGraph(name="Employee.full", ...)
#Entity
public class Employee {
private Set<Role> roles = new HashSet<>();
}
#Entity
public class Role {
private Set<Employee> employees = new HashSet<>();
}
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
#EntityGraph(value = "Employee.full")
#Override
Page<Employee> findAll(Pageable pageable);
}
#Service
public class EmployeeService {
public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
Page<Employee> employees = repository.findAll(pageRequest); // ok
Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext()); // this is where the exception happens
return dtos;
}
}
// also there is EmployeeDTO and RoleDTO classes mirroring the entity classes
// and there is a simple interface EmployeeMapper loaded as a spring component
// without any special mappings. However CycleAvoidingMappingContext is used.
I have tracked down the LazyInitializationException to happen when the mapper tries to map the roles dependency. The Role object do have Set<Employee> and therefore there is a cyclic reference.
When using FetchType.EAGER new CycleAvoidingMappingContext() solved this problem, but with LAZY this no longer works.
Does anybody know how I can avoid the exception and at the same time get my DTOs mapped correctly?
The problem is that when the code returns from findAll the entities are not managed anymore. So you have a LazyInitializationException because you are trying, outside of the scope of the session, to access a collection that hasn't been initialized already.
Adding eager make it works because it makes sure that the collection has been already initialized.
You have two alternatives:
Using an EAGER fetch;
Make sure that the entities are still managed when you return from the findAll. Adding a #Transactional to the method should work:
#Service
public class EmployeeService {
#Transactional
public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
Page<Employee> employees = repository.findAll(pageRequest);
Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext());
return dtos;
}
}
I would say that if you need the collection initialized, fetching it eagerly (with an entity graph or a query) makes sense.
Check this article for more details on entities states in Hibernate ORM.
UPDATE: It seems that this error happens because Mapstruct is converting the collection even if you don't need it in the DTO.
In this case, you have different options:
Remove the field roles from the DTO. Mapstruct will ignore the field in the entity because the DTO doesn't have a field with the same name;
Create a different DTO class for this specific case without the field roles;
Use the #Mapping annotation to ignore the field in the entity:
#Mapping(target = "roles", ignore = true)
void toDTO(...)
or, if you need the toDTO method sometimes
#Mapping(target = "roles", ignore = true)
void toSkipRolesDTO(...) // same signature as toDTO
I have entities that look like the following:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class MyEntity {
#Id
private UUID id;
#OneToMany(targetEntity = Relation.class, fetch = FetchType.EAGER)
private List<Relation> relatedList;
}
#Entity
#Data
public class Relation {
#Id
private UUID id;
}
In addition, I have another type:
#Data
public class OtherType extends MyEntity {
private String otherField;
public OtherType(UUID id, List<Relation> relations, String otherField) {
super(id, relations);
this.otherField = otherField;
}
}
What I want to do now is to select the objects in the table of MyEntity together with some additional info (otherField) into an object of type OtherType:
select e.id, e.relatedList, 'otherStuff' as otherField from MyEntity e
If I use this query with HQL, it converts e.relatedList to . as col_x_x_, which obviously is a syntax error. I was trying to use a native query, but that just says that OtherType is not an Entity. If I use a NamedNativeQuery with a resultSetMapping, it can't map a list of values to a Collection (No dialect mapping for JDBC type 1111). What I also tried is use the postgres array_agg function to get only an array of IDs for my relation, but that can't be mapped either. Is there any way to achieve this except defining a constructor in OtherType that accepts a single value instead of a list, doing an actual real SQL join (where every instance of Relation adds another MyEntity row), and mapping that afterwards?
This is a perfect use case for Blaze-Persistence Entity Views.
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. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.
The interesting part for you is, that it supports collections. A sample model could look like the following:
#EntityView(MyEntity.class)
public interface MyEntityView {
#IdMapping
UUID getId();
String getOtherField();
List<RelationView> getRelations();
}
#EntityView(Relation.class)
public interface RelationView {
#IdMapping
UUID getId();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
MyEntityView p = entityViewManager.find(entityManager, MyEntityView.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
First, here are my entities.
Player :
#Entity
#JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class,
property="id")
public class Player {
// other fields
#ManyToOne
#JoinColumn(name = "pla_fk_n_teamId")
private Team team;
// methods
}
Team :
#Entity
#JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class,
property="id")
public class Team {
// other fields
#OneToMany(mappedBy = "team")
private List<Player> members;
// methods
}
As many topics already stated, you can avoid the StackOverflowExeption in your WebService in many ways with Jackson.
That's cool and all but JPA still constructs an entity with infinite recursion to another entity before the serialization. This is just ugly ans the request takes much longer. Check this screenshot : IntelliJ debugger
Is there a way to fix it ? Knowing that I want different results depending on the endpoint. Examples :
endpoint /teams/{id} => Team={id..., members=[Player={id..., team=null}]}
endpoint /members/{id} => Player={id..., team={id..., members=null}}
Thank you!
EDIT : maybe the question isn't very clear giving the answers I get so I'll try to be more precise.
I know that it is possible to prevent the infinite recursion either with Jackson (#JSONIgnore, #JsonManagedReference/#JSONBackReference etc.) or by doing some mapping into DTO. The problem I still see is this : both of the above are post-query processing. The object that Spring JPA returns will still be (for example) a Team, containing a list of players, containing a team, containing a list of players, etc. etc.
I would like to know if there is a way to tell JPA or the repository (or anything) to not bind entities within entities over and over again?
Here is how I handle this problem in my projects.
I used the concept of data transfer objects, implemented in two version: a full object and a light object.
I define a object containing the referenced entities as List as Dto (data transfer object that only holds serializable values) and I define a object without the referenced entities as Info.
A Info object only hold information about the very entity itself and not about relations.
Now when I deliver a Dto object over a REST API, I simply put Info objects for the references.
Let's assume I deliever a PlayerDto over GET /players/1:
public class PlayerDto{
private String playerName;
private String playercountry;
private TeamInfo;
}
Whereas the TeamInfo object looks like
public class TeamInfo {
private String teamName;
private String teamColor;
}
compared to a TeamDto
public class TeamDto{
private String teamName;
private String teamColor;
private List<PlayerInfo> players;
}
This avoids an endless serialization and also makes a logical end for your rest resources as other wise you should be able to GET /player/1/team/player/1/team
Additionally, the concept clearly separates the data layer from the client layer (in this case the REST API), as you don't pass the actually entity object to the interface. For this, you convert the actual entity inside your service layer to a Dto or Info. I use http://modelmapper.org/ for this, as it's super easy (one short method call).
Also I fetch all referenced entities lazily. My service method which gets the entity and converts it to the Dto there for runs inside of a transaction scope, which is good practice anyway.
Lazy fetching
To tell JPA to fetch a entity lazily, simply modify your relationship annotation by defining the fetch type. The default value for this is fetch = FetchType.EAGER which in your situation is problematic. That is why you should change it to fetch = FetchType.LAZY
public class TeamEntity {
#OneToMany(mappedBy = "team",fetch = FetchType.LAZY)
private List<PlayerEntity> members;
}
Likewise the Player
public class PlayerEntity {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "pla_fk_n_teamId")
private TeamEntity team;
}
When calling your repository method from your service layer, it is important, that this is happening within a #Transactional scope, otherwise, you won't be able to get the lazily referenced entity. Which would look like this:
#Transactional(readOnly = true)
public TeamDto getTeamByName(String teamName){
TeamEntity entity= teamRepository.getTeamByName(teamName);
return modelMapper.map(entity,TeamDto.class);
}
In my case I realized I did not need a bidirectional (One To Many-Many To One) relationship.
This fixed my issue:
// Team Class:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Player> members = new HashSet<Player>();
// Player Class - These three lines removed:
// #ManyToOne
// #JoinColumn(name = "pla_fk_n_teamId")
// private Team team;
Project Lombok might also produce this issue. Try adding #ToString and #EqualsAndHashCode if you are using Lombok.
#Data
#Entity
#EqualsAndHashCode(exclude = { "members"}) // This,
#ToString(exclude = { "members"}) // and this
public class Team implements Serializable {
// ...
This is a nice guide on infinite recursion annotations https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
You can use #JsonIgnoreProperties annotation to avoid infinite loop, like this:
#JsonIgnoreProperties("members")
private Team team;
or like this:
#JsonIgnoreProperties("team")
private List<Player> members;
or both.
I'm on Spring boot 1.4.x branch and Spring Data MongoDB.
I want to extend a Pojo from HashMap to give it the possibility to save new properties dynamically.
I know I can create a Map<String, Object> properties in the Entry class to save inside it my dynamics values but I don't want to have an inner structure. My goal is to have all fields at the root's entry class to serialize it like that:
{
"id":"12334234234",
"dynamicField1": "dynamicValue1",
"dynamicField2": "dynamicValue2"
}
So I created this Entry class:
#Document
public class Entry extends HashMap<String, Object> {
#Id
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
And the repository like this:
public interface EntryRepository extends MongoRepository<Entry, String> {
}
When I launch my app I have this error:
Error creating bean with name 'entryRepository': Invocation of init method failed; nested exception is org.springframework.data.mapping.model.MappingException: Could not lookup mapping metadata for domain class java.util.HashMap!
Any idea?
TL; DR;
Do not use Java collection/map types as a base class for your entities.
Repositories are not the right tool for your requirement.
Use DBObject with MongoTemplate if you need dynamic top-level properties.
Explanation
Spring Data Repositories are repositories in the DDD sense acting as persistence gateway for your well-defined aggregates. They inspect domain classes to derive the appropriate queries. Spring Data excludes collection and map types from entity analysis, and that's why extending your entity from a Map fails.
Repository query methods for dynamic properties are possible, but it's not the primary use case. You would have to use SpEL queries to express your query:
public interface EntryRepository extends MongoRepository<Entry, String> {
#Query("{ ?0 : ?1 }")
Entry findByDynamicField(String field, Object value);
}
This method does not give you any type safety regarding the predicate value and only an ugly alias for a proper, individual query.
Rather use DBObject with MongoTemplate and its query methods directly:
List<DBObject> result = template.find(new Query(Criteria.where("your_dynamic_field")
.is(theQueryValue)), DBObject.class);
DBObject is a Map that gives you full access to properties without enforcing a pre-defined structure. You can create, read, update and delete DBObjects objects via the Template API.
A last thing
You can declare dynamic properties on a nested level using a Map, if your aggregate root declares some static properties:
#Document
public class Data {
#Id
private String id;
private Map<String, Object> details;
}
Here we can achieve using JSONObject
The entity will be like this
#Document
public class Data {
#Id
private String id;
private JSONObject details;
//getters and setters
}
The POJO will be like this
public class DataDTO {
private String id;
private JSONObject details;
//getters and setters
}
In service
Data formData = new Data();
JSONObject details = dataDTO.getDetails();
details.put("dynamicField1", "dynamicValue1");
details.put("dynamicField2", "dynamicValue2");
formData.setDetails(details);
mongoTemplate.save(formData );
i have done as per my business,refer this code and do it yours. Is this helpful?