I have a Spring Boot App with a neo4j database and have a problem with the neo4j repository fetching nested data and creating a loop.
I have a node Person. A Person can send access requests to another Person. When I fetch a Person, I want to get the data of all the access requests TO and FROM other Persons. The model looks like this:
#Data
#Node
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
#Id
#GeneratedValue
private Long id;
private String email;
#Relationship(type = "ACCESS_REQUESTED", direction = Relationship.Direction.OUTGOING)
#JsonProperty("requested_contacts")
private List<AccessRequested> requestedContacts;
#Relationship(type = "ACCESS_REQUESTED", direction = Relationship.Direction.INCOMING)
#JsonProperty("contact_requests")
private List<AccessRequested> contactRequests;
}
#RelationshipProperties
#Data
public class AccessRequested {
#RelationshipId
#GeneratedValue
#JsonIgnore
private Long id;
#TargetNode
private Person person;
private String reason;
}
Now I am using the Neo4jRepository like this:
#Repository
public interface PersonRepository extends Neo4jRepository<Person, Long> {}
And I am calling findById(Long id)
The problem is that when I fetch a Person that has sent the request to another person, it creates an infinite loop:
{
"id": 13,
"requested_contacts": [],
"contact_requests": [
{
"person": {
"id": 5,
"requested_contacts": [
{
"person": {
"id": 13,
"requested_contacts": [],
"contact_requests": [
{
"person": {
"id": 5,
"contact_cards": [],
"requested_contacts": [
{
"person": {
"id": 13,
"requested_contacts": [],
"contact_requests": [
{
"person": {
"id": 5,
"contact_cards": [],
"requested_contacts": [
{
"person": {
"id": 13,
…….
How can I fix that? I want to fetch the full person on top level, but the nested persons should only contain id and email.
Related
I have 2 tables with structures like this :
Table 1: user
id, // Primary key
name,
email
Table 2: user_social
user_id, // Foreign key to user table
social_channel,
social_link
The data is as follows:
user:
1, "Ram", "ram#gmail.com"
user_social:
1, "FB", "fb.com/ram"
1, "INSTA", "insta.com/ram"
And the respective JPA entities classes:
User.java
#Entity
#Table(name = "user")
public class User {
#Id
UUID id;
String name;
String email;
#OneToMany(mappedBy = "id.userObj")
Set<UserSocial> userSocial = new HashSet<>();
}
UserSocial.java
#Entity
#Table(name = "user_social")
public class UserSocial {
// there is no primary key to the table and the whole row is unique
#EmbeddedId
UserSocialId id;
#Embeddable
public static class UserSocialId {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
User userObj;
String social_link;
String social_channel
}
}
Now when I try to fetch all the user details, I get duplicate results like this:
[
{
"id": 1,
"name": "Ram",
"email": "ram#gmail.com",
"userSocial": [
{ "social_link": "fb.com/ram", "social_channel": "FB" },
{ "social_link": "insta.com/ram", "social_channel": "INSTA" }
]
},
{
"id": 1,
"name": "Ram",
"email": "ram#gmail.com",
"userSocial": [
{ "social_link": "fb.com/ram", "social_channel": "FB" },
{ "social_link": "insta.com/ram", "social_channel": "INSTA" }
]
}
]
I believe this is because JPA makes a join query and since the user_social table has 2 rows in it, it duplicates the result for it. Is there a way where we could aggregate the user_social results in a list and thus have a single JSON element in response?
EDIT:
I am using an interface interface UserRepository extends JpaRepository<User, UUID> 's method findAll method to fetch results.
I have the following entities:
#Entity
#Table(name = "business", schema = "public")
public class Business {
// some properties
}
#Entity
#Table(name = "appuser", schema = "public")
public class AppUser implements UserDetails {
// some properties
#JsonManagedReference
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<UserBusinessRole> userBusinessRoles = new ArrayList<>();
}
#Entity
#Table(name = "appuser_business_role", schema = "public")
public class UserBusinessRole {
// some properties
#ManyToOne
#JoinColumn(name = "business_id")
private Business business;
}
These work without issue when calling individually, however, I also have an entity that has business AND app user:
#Entity
#Table(name = "import_session", schema = "public")
public class ImportSession {
// some properties
#JsonIgnore
#ManyToOne()
#JoinColumn(
name = "requester_user_id",
referencedColumnName = "id"
)
private AppUser requester;
#ManyToOne
#JoinColumn(name = "business_id")
private Business business;
}
But it returns duplicate values for business like below (listed under roles and in the root object):
{
"id": 14,
...
"requesterDto": {
"id": 123,
"emailAddress": "bar#bar.com",
"userBusinessRolesDto": [
{
"id": 6,
"type": "ADMIN",
"businessDto": {
"name": "Foo Inc"
...
}
}
]
},
"businessDto": {
"name": "Foo Inc"
}
}
Is there a way to make it ONLY return certain fields, or control how 'deep' it populates, without a lot of manual fiddeling / creating separate DTOs all over? So it would look something like this for example:
{
"id": 14,
...
"requesterDto": {
"id": 123,
"emailAddress": "bar#bar.com"
},
"businessDto": {
"name": "Foo Inc"
...
}
}
I am using Spring Data JPA and Spring Data Rest.
When making a REST request to persist an entity, I get the next error:
org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value
My data model has the following entities:
Contract:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING)
public class Contract implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY,
mappedBy="contract"
)
private List<Participation> participants = new ArrayList<Participation>();
private String name;
}
Participation:
#Entity
public class Participation implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(nullable = false) //By default the column will be CONTRACT_ID
private Contract contract;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(nullable = false)
private Contact contact;
private String clauses;
}
Contact:
#Entity
public class Contact implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String emailAddress;
}
I have 2 JPARepositories:
public interface ContractRepository extends JpaRepository<Contract, Long> {
List<Contract> findByNameContainsIgnoreCase(String name);
}
public interface ContactRepository extends JpaRepository<Contact, Long> {
}
To save a new Contract with a couple of participations, I am doing the next steps in Postman:
Create a Contract and get its href:
Request: POST http://localhost:8080/api/contracts
Body:
{
"name": "Contract1"
}
The response is successful:
201 Created
{
"name": "Contract1",
"participants": [],
"_links": {
"self": {
"href": "http://localhost:8080/api/contracts/4"
},
"contract": {
"href": "http://localhost:8080/api/contracts/4"
},
}
}
So far so good. Now that I have the contract persisted, I am adding participants:
Contact 1 already exists in the data base.
Request: PATCH http://localhost:8080/api/contracts/4
Body:
{
"participants": [
{
"clauses": "Bla bla bla",
"contact": {
"href": "http://localhost:8080/api/contacts/1"
},
"contract": {
"href": "http://localhost:8080/api/contracts/4"
}
}
]
}
When executing this request the system complains on the field/fk contract:
{
"cause": {
"cause": null,
"message": "not-null property references a null or transient value : com.xxx.xxx.model.Participation.contract"
},
"message": "not-null property references a null or transient value : com.xxx.xxx.model.Participation.contract; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : com.xxx.xxx.model.Participation.contract"
}
I tried several ways to reference the contract in the participation, like:
"contract": "http://localhost:8080/api/contracts/4"
No luck. For some reason the system is leaving the field empty instead of using the foreing key of the entity created in step 1.
What am I doing wrong?
The problem can be solved by:
Add a new repository ParticipationRepository (extends JpaRepository);
Create first a Contract without Participations:
POST http://localhost:8080/api/contracts { "name": "Contract1" }
Response:
201 Created
{
"name": "Contract1",
"_links": {
"self": {
"href": "http://localhost:8080/api/contracts/3"
},
"contract": {
"href": "http://localhost:8080/api/contracts/3"
},
"participants": {
"href": "http://localhost:8080/api/contracts/3/participants"
}
}
}
Create a Participation and use the URI from the just created Contract to set the FK. Assume Contact 1 already exists in the Data Base.
POST http://localhost:8080/api/participations {
"clauses": "bla, bla, bla",
"contract": "http://localhost:8080/api/contracts/3",
"contact": "http://localhost:8080/api/contacts/1" }
Response:
201 Created
{
"clauses": "bla, bla, bla",
"_links": {
"self": {
"href": "http://localhost:8080/api/participations/5"
},
"participation": {
"href": "http://localhost:8080/api/participations/5"
},
"contract": {
"href": "http://localhost:8080/api/participations/5/contract"
},
"contact": {
"href": "http://localhost:8080/api/contacts/5/contact"
}
}
}
I have the following in my code:
CompanyEntity
#Entity
#Table(name = "company")
public class Company{
#OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
#JsonUnwrapped
private Set<User> users;
}
UserEntity
#Entity
#Table(name="user")
public class User{
#ManyToOne(cascade = CascadeType.REFRESH)
#JoinColumn(name="company_id")
private Company company;
}
CompanyController
#GetMapping("/company")
public ResponseEntity<Object> getAllCompanies(){
List<Company> allCompanies = companyService.findAll();
return ResponseEntity.ok(allCompanies);
}
problem is when i call /company in the browser i am getting the users object including the company object. something like this
[
{
"id": 1,
"name": "company",
"users": [
{
"id": 14,
"firstName": "Yamen",
"lastName": "Nassif",
"company": {
"id": 1,
"name": "company",
"users": [
{
"id": 14,
"firstName": "Yamen",
"lastName": "Nassif",
"company": {
"id": 1,
"name": "company",
"users": [
...
same goes when i getAllUsers companies and users are also exanding.
my database looks just fine.
and its endless and of course Stackoverflow error is in the console. How can i fix this ?
You have this error because of the infinite recursion.
Company has a link on User and User has a link on Company.
You have at least two options:
use #JsonManagedReference and #JsonBackReference annotation on the relation fields.
create a pair of DTOs and fill them manually with data from you entities.
e.g.
#GetMapping("/company")
public ResponseEntity<Object> getAllCompanies() {
List<Company> allCompanies = companyService.findAll();
List<CompanyDto> allCompanyDtoList = convertToCompanyDtoList(allCompanies);
return ResponseEntity.ok(allCompanyDtoList );
}
Personally, I'd prefer the second option, since returning Entities is NOT a good practice.
You can use #JsonIgnore annotation to prevent this type of behavior. This usually happens with bidirectional mapping within your entities. It is caused by infinite recursion.
#Entity
#Table(name="user")
public class User{
#ManyToOne(cascade = CascadeType.REFRESH)
#JoinColumn(name="company_id")
#JsonIgnore
private Company company;
}
I want to populate the repository with user roles and two initial users related to those roles.
This is the JSON I want to upload:
[
{
"_class": "models.Role",
"#id": 1,
"name": "ROLE_BLOG_ADMIN",
"description": "Rol de los administradores del blog"
},
{
"_class": "models.Role",
"#id": 2,
"name": "ROLE_BLOG_CONTRIBUTOR",
"description": "Rol de los editores de artículos"
},
{
"_class": "models.User",
"username": "sergio11",
"password": "$2a$10$0eCQpFRdw8i6jJzjj/IuNuKpJYnLaO5Yp9xSJ3itcfPmQNXVhmNyu",
"email": "gfhdsgfjhdsgfjhdsgf#gmail.com",
"fullName": "Sergio Sánchez Sánchez",
"roles": [1, 2]
},
{
"_class": "models.User",
"username": "dani33",
"password": "$2a$10$0eCQpFRdw8i6jJzjj/IuNuKpJYnLaO5Yp9xSJ3itcfPmQNXVhmNyu",
"email": "danihiglesias#usal.es",
"fullName": "Daniel de la Iglesia",
"roles": [2]
}
]
I am using JsonIdentityInfo in the Roles entity:
#Entity
#Table(name = "roles")
#JsonIdentityInfo(generator=IntSequenceGenerator.class, property="#id")
public class Role implements Serializable
I have included Jackson2RepositoryPopulatorFactoryBean in the context:
#Bean(name="repositoryPopulator")
public Jackson2RepositoryPopulatorFactoryBean provideJackson2RepositoryPopulatorFactoryBean(){
Resource sourceData = new ClassPathResource("data.json");
Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
factory.setResources(new Resource[] { sourceData });
return factory;
}
But, no role is associated with any user.
This is the association:
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name="USER_ROLES",
joinColumns=#JoinColumn(name="USER_ID", referencedColumnName="ID"),
inverseJoinColumns=#JoinColumn(name="ROLE_ID", referencedColumnName="ID"))
private Set<Role> roles;
Does anyone know how to fix them?
It should only complain about a missing constructor for the Role entity, for me everything went smoothly with the following :
#Entity
#Table(name = "roles")
public class Role implements Serializable {
public Role() {
}
public Role(Integer id) {
this.id = id;
}
#Id
#JsonProperty("#id")
private Integer id;
//additional properties, getters & setters
...
}
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name="USER_ROLES",
joinColumns=#JoinColumn(name="USER_ID", referencedColumnName="ID"),
inverseJoinColumns=#JoinColumn(name="ROLE_ID", referencedColumnName="ID"))
private Set<Role> roles;
//additional properties, getters & setters
...
}
And I properly get :
[
{
"id":1,
"roles":[
{
"name":"ROLE_BLOG_ADMIN",
"#id":1
},
{
"name":"ROLE_BLOG_CONTRIBUTOR",
"#id":2
}
],
"email":"sss4esob#gmail.com"
},
{
"id":2,
"roles":[
{
"name":"ROLE_BLOG_CONTRIBUTOR",
"#id":2
}
],
"email":"danihiglesias#usal.es"
}
]
Can you provide more of your entities code if this isn't working for you ? Do you encounter any exception ?