I have two entities, Company and Job, with an OneToMany bidirectional relationship. My problem is that i can't lazy load the Company's List<Job> jobs.
For example when i do:
GET /api/companies/1 this is the JSON response:
{
"id": 1,
"name": "foo",
...
"_embedded": {
"jobs": [
{...},
...
{...}
],
"employees": [
{...},
{...}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/companies/1"
},
"jobs": {
"href": "http://localhost:8080/api/companies/1/jobs"
},
"employees": {
"href": "http://localhost:8080/api/companies/1/employees"
}
}
}
I don't want to have the _embedded since i didn't set the FetchType=EAGER.
Here are my models:
Company.java
#Entity
public class Company {
#Column(nullable = false, unique = true)
private String name;
#OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
private List<Job> jobs;
...
public Company() {
}
...
}
Job.java
#Entity
public class Job {
#Column(nullable = false)
public String title;
#Column(length = 10000)
public String description;
#ManyToOne(fetch=FetchType.LAZY)
private Company company;
...
public Job() {
}
...
}
As you can see the same thing happens for other OneToMany relationships (employees). Can i avoid returning the whole list of job openings or employees every time?
EDIT: from the Job side the lazy load works fine! I don't get in the response the company that is related with a Job. I have to explicitly do /api/jobs/123/company in order to get the company.
EDIT2: Projections only work for collections. In this case it's not what i need. Excerpts could work, but i want to avoid them. I don't want to explicilty do /api/companies/1?projection=MyProjection since i won't use more than one. I want to change the default behavior, just like the projections do in collections.
EDIT3: i tried this
#RestResource(exported = false)
#OneToMany(mappedBy = "company")
private List<Job> jobs;
and i get the error Detected multiple association links with same relation type! Disambiguate association.
it's really annoying. I just need to get rid of _embedded. Anything?
You can use Entity Graph.Entity graphs are used to override at runtime the fetch settings of attribute mappings.For example
#Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
#EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
From Spring Data Jpa Documentation "4.3.10. Configuring Fetch- and LoadGraphs"
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
In addition;
Related
I'm working on a Spring Data Rest based Spring Boot service whose data model is similar to the one in the tutorial:
https://www.baeldung.com/spring-data-rest-relationships
(In defining the entities, I'm using Lombok annotations):
#Data
#NoArgsConstructor
#Entity
#Table(name = "cale")
public class Book {
#Id
#GeneratedValue
private long id;
#Column(nullable=false)
private String title;
#ManyToOne
private Library library;
}
#Data
#NoArgsConstructor
#Entity
#Table(name = "library")
public class Library {
#Id
#GeneratedValue
private long id;
//...
}
invoking the endpoint /books I get the following json:
{
"_embedded": {
"books": [
{
"id": 22,
"title": "The title of the book",
"_links": {
"self": {
"href": "http://192.168.33.20:8080/books/22"
},
"book": {
"href": "http://192.168.33.20:8080/books/22"
},
"library": {
"href": "http://192.168.33.20:8080/books/22/library"
}
}
},
.
.
.
in HAL style, the only reference to the library linked to a given book is through an URL like http://192.168.33.20:8080/books/22/library
In order to get the id of the library associated to book 22, I have to perform a second GET call to the URL, which is inefficient in many cases.
Moreover, this makes it very hard to implement queries like "GET all books associated to the library whose id is 101".
Is there a way to let Spring Data Rest include also the id of the associated entity into the returned json? Something like:
{
"_embedded": {
"books": [
{
"id": 22,
"title": "The title of the book",
"library_id": 101,
"_links": {
.
.
.
}
},
.
.
You can create a projection and use it by default with an excerpt.
To define the projection :
#Projection(
name = "customBook",
types = { Book.class })
public interface CustomBook {
#Value("#{target.id}")
long getId();
String getTitle();
#Value("#{target.getLibrary().getId()}")
int getLibraryId();
}
And call :
http://192.168.33.20:8080/books/22?projection=customBook
To use this projection by default configure your repo :
#RepositoryRestResource(excerptProjection = CustomBook.class)
public interface BookRepository extends CrudRepository<Book, Long> {}
I'm working on a small Spring REST API project. I have two classes that represent 2 tables in my database. I have a #OneToMany mapping in the object that I want to retrive data from. Right now I retrive ALL the nested objects, but what I want is to be able to limit the amount of nested objects by its int datestamp variable (which is an epoch declared as "utcs" in my class). I was naively thinking that the CrudRepoitory could help me with that but now I understand I was wrong. What I was hoping to be able to do in my repository was something like this:
#Repository
public interface TypeRepository extends CrudRepository<Type, Integer> {
List<Type> findByDataUtcsGreaterThan(int utcs);
}
This is the JSON structure I want and how it looks right now. But how do I limit the amount of Data objects?
[
{
"typeKey": "Queue",
"uomId": 1,
"data": [
{
"value": 11,
"utcs": 1605840300
},
{
"value": 15,
"utcs": 1605840360
},
{
"value": 22,
"utcs": 1605840420
}
]
},
{
"typeKey": "Unroutable",
"uomId": 1,
"data": [
{
"value": 196,
"utcs": 1605840300
},
{
"value": 196,
"utcs": 1605840360
},
{
"value": 196,
"utcs": 1605840420
}
]
}
]
The (Type) object class with the nested object #OneToMany
#Entity
#Table(name = "SYSTEMSTATSTYPE")
public class Type {
#Id
#Column(name = "ID")
private int id;
#Column(name = "TYPEKEY")
private String typeKey;
#Column(name = "UOMID")
private int uomId;
#OneToMany(mappedBy = "type", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<Data> data = new ArrayList<>();
public Type() {
super();
}
public Type(String typeKey, int uomId) {
this.typeKey = typeKey;
this.uomId = uomId;
}
// Getters and setters
}
The (Data) object class #ManyToOne
#Entity
#Table(name = "SYSTEMSTATSDATA")
public class Data {
#Id
#Column(name = "ID")
private int id;
#Column(name = "VALUE")
private int value;
#Column(name = "UTCS")
private int utcs;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "TYPEID")
private Type type;
public Data() {
super();
}
public Data(int value, int utcs, Type type) {
super();
this.value = value;
this.utcs = utcs;
this.type = type;
}
// Getters and setters
}
I don't think there is a possibility directly with the annotation, and using EAGER fetching is in most cases not a good idea anyway. I think you have to build the logic yourself with a custom query, either fetching only the 2 Data objects (something as described here https://www.baeldung.com/jpa-limit-query-results), or a join getting you Type and Data all together right away.
That's not directly possible. You could do dedicated select queries for each Type object or maybe use something like Blaze-Persistence Entity Views which has native support for limiting collection elements.
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(Type.class)
public interface TypeDto {
#IdMapping
Integer getId();
String getTypeKey();
int getUomId();
#Limit(limit = "5", order = "utcs DESC")
Set<DataDto> getData();
#EntityView(Data.class)
interface DataDto {
#IdMapping
Integer getId();
int getValue();
int getUtcs();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
TypeDto a = entityViewManager.find(entityManager, TypeDto.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
#Repository
public interface TypeRepository extends CrudRepository<Type, Integer> {
List<TypeDto> findByDataUtcsGreaterThan(int utcs);
}
I have simple scenario where there is relation between User and Skill,
means one user many skills, so I tried with:
User
#Data
#NoArgsConstructor
#Entity
#EqualsAndHashCode
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#OneToMany(mappedBy = "user")
private List<Skill> skills;
}
Skill
#Data
#NoArgsConstructor
#Entity
#EqualsAndHashCode
public class Skill {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String skillTitle;
#ManyToOne
#JoinColumn(name="user_id")
private User user;
}
UserRepository
#RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
List<User> findByName(#Param("name") String name);
}
SkillRepository
#RepositoryRestResource(collectionResourceRel = "skills", path = "skills")
public interface SkillRepository extends CrudRepository<Skill, Long>{
}
with all above I'm able to get response at for example url http://localhost:8085/users/1
{
"name": "Root",
"_links": {
"self": {
"href": "http://localhost:8085/users/1"
},
"user": {
"href": "http://localhost:8085/users/1"
},
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
}
}
not the issue is I'm not figuring out why list of skills is not fetched, why only this is fetched
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
not a full list of skills related to user/1.
UPDATE
Added projection as suggested:
UserProjection.java
#Projection(name = "inlineData", types=User.class)
public interface UserProjection {
String getName();
List<Skill> getSkills();
}
UserRepository.java is
#RepositoryRestResource(collectionResourceRel = "users", path = "users", excerptProjection = UserProjection.class)
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
List<User> findByName(#Param("name") String name);
}
response is:
{
"name": "Root",
"_links": {
"self": {
"href": "http://localhost:8085/users/1"
},
"user": {
"href": "http://localhost:8085/users/1{?projection}",
"templated": true
},
"skills": {
"href": "http://localhost:8085/users/1/skills"
}
}
}
The response is correct, it works as intended. #RepositoryRestResource follows HATEOAS principles. Spring documentation explains it as follows:
5.1.3. Resource Discoverability
A core principle of HATEOAS is that resources should be discoverable
through the publication of links that point to the available
resources ...
By issuing a request to the root URL ... the client can extract, from
the returned JSON object, a set of links that represent the next level
of resources that are available to the client ...
You get links that represent resources. To retrieve specific resources you should call corresponding URL. Your response for User 1 means that if you want to get Skills of User 1 you should call URL "http://localhost:8085/users/1/skills".
It is easier to understand it if you imagine that you have an HTML page that displays properties of User 1. This page does not directly display Skills, instead this page contains a link to Skills page. Only if a user clicks on this link the Skills page will be loaded.
It is important that you understand HATEOAS well.
Of course there can be cases when HATEOAS is not the best choice. But here we are not discussing HATEOAS, but explaining what is the idea behind this implementation of Spring. This approach can really be helpful in many cases. When you have 2 entities with 1-2 attributes, you may consider such approach as overkill. But if you have 30 - 50 entities, each with 3 - 5 relations, each relation containing 50 - 100 other entities, it can be quite hard to deal with such data model. And HATEOAS can make it much easier. With this approach you are just navigating these relations: load one entity, select needed relation, load entities on this relation, select needed entity, in this entity select needed relation, load this relation, or navigate back to its parent entity via parent relation, etc.
maybe another many-to-many relationship issue with spring data-jpa and how to update an existing entity with another existing entities.
I'll put a short version of my Entities just for clarify only when the error occurs.
I have a Peticion entity:
#Entity
#Table(name = "peticiones")
#JsonIdentityInfo(generator =
ObjectIdGenerators.PropertyGenerator.class,property = "id", scope =
Peticion.class)
#Validated
public class Peticion
{
private int id;
private Usuario usuario;
private Categoria categoria;
private Set<Tag> tags;
#ManyToMany( fetch = FetchType.LAZY, cascade = {
CascadeType.MERGE
} )
#JoinTable( name="peticion_tag", joinColumns= #JoinColumn(name = "peticion_id", referencedColumnName="id"), inverseJoinColumns = #JoinColumn(name="tag_id", referencedColumnName="id") )
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
And Tag entity:
#Entity
#Table(name = "tags")
#JsonIdentityInfo(generator =
ObjectIdGenerators.PropertyGenerator.class,property = "id", scope =
Tag.class)
public class Tag
{
private int id;
#Size( min = 4 )
private String nombre;
private Set<Peticion> peticiones;
private Set<Categoria> categorias;
private Set<Usuario> usuarios;
#ManyToMany( mappedBy="tags" )
public Set<Peticion> getPeticiones() {
return peticiones;
}
public void setPeticiones(Set<Peticion> peticiones) {
this.peticiones = peticiones;
}
Ok, so when I try to put or patch one Peticion in the format of:
{
"id": 123,
"usuario":{
"id": 5
},
"categoria":{
"id": 7
},
"tags":[
{
"id":3
},
{
"id":10
}
]
}
When I send this information, I got an error that says that I have a constraint violation saying that name I suppose the one property for Tag is null... So I figure it out that this is trying to create another entity, but that's not the case I wanna do, I wanna update the relationships between Peticion and Tag, and for example if I do this:
{
"id": 123,
"usuario":{
"id": 5
},
"categoria":{
"id": 7
},
"tags":[]
}
It works perfectly, I mean it deletes the relationship tags that were before. So I don't know if I'm sending the json correctly or do I have to put another configuration annotation or something in my Entities.
Note: I'm using JpaRepository for saving/updating and my controller only calls the method save.
Thank you
You might be missing fetches on the relation entity on your database layer, I think you should also share the repositories and queries that you use to fetch the linked data.
I building an api rest with spring boot. I have a parent-child relationship in witch the child its an array of objects.
The problem is that deserialization only picks the first item of the array. Everything else seems to work fine. The parent and the child are pesisted in the database too.
I send something like this:
"user": {
"name": "foo",
"childs": [
{
"name": "bar",
....
},
{
"name": "foobar",
....
}
],
....
}
But got persisted this:
"user": {
"id": 1,
"name": "foo",
"childs": [
{
"id": 1,
"name": "bar",
....
}
],
....
}
Any clue on this?
Update
Parent Entity:
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = User.class)
#Entity( name = "users" )
#Table( name = "users" )
public class User extends ModelEntity {
Model's fields...
...
#JsonView( value = {DTOViews.PrivateProfile.class, DTOViews.Owner.class} )
#JsonManagedReference( value = "User-ProfessionalExperience" )
#OneToMany( mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY )
private Set<ProfessionalExperience> professionalExperiences;
}
Child entity:
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = ProfessionalExperience.class)
#Entity
#Table( name = "professional_experiences")
public class ProfessionalExperience extends ModelEntity {
Model's fields...
...
#JsonBackReference( value = "User-ProfessionalExperience" )
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User user;
}
Controller:
#RequestMapping(method = RequestMethod.POST)
public MappingJacksonValue create(#RequestBody #Valid User userToCreate, BindingResult result) {
...
}
Thank you all in advance.
So, I finally solved it. The issue comes from the relationship collection type and hasCode() / equals() methods.
All the entities in my model extend from "ModelEntity" class. This class provides id and record active fields for all extending models and a hasCode/equals method based on these fields. As the relationship between "User" and "ProfessionalExperience" is defined as a set, it can't store duplicated elements.
So, to tell jackson that the children are different elements, we need to override hasCode/equals in each model class with the fields defined in each one.