I have entities with bidirectional mapping to each other. Calling REST Http.GET request to get all records from db, I am receiving StackOverflowException due to infinite recursion. I was trying to use #JsonIgnore, #JsonBackReference together with #JsonManageReference and #JsonIdentityInfo in different combinations, but with no positive result. I am still receiving the error.
Spring Boot loads me jackson in version 2.6.6.
Here is my BaseEntity:
#MappedSuperclass
public class BaseEntity {
#Id
#GeneratedValue
private Long id;
private String createdBy;
private Date createdOn;
private String modifiedBy;
private Date modifiedOn;
public String description;
public BaseEntity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Date getCreatedOn() {
return createdOn;
}
public void setCreatedOn(Date createdOn) {
this.createdOn = createdOn;
}
public String getModifiedBy() {
return modifiedBy;
}
public void setModifiedBy(String modifiedBy) {
this.modifiedBy = modifiedBy;
}
public Date getModifiedOn() {
return modifiedOn;
}
public void setModifiedOn(Date modifiedOn) {
this.modifiedOn = modifiedOn;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
First Entity class:
#Entity
public class Entry extends BaseEntity{
private Date businessOperationDate;
#ManyToOne
private Version version;
#ManyToOne
private Status status;
#ManyToOne
#JsonManagedReference
private Account account;
public Entry() {
}
public Date getBusinessOperationDate() {
return businessOperationDate;
}
public void setBusinessOperationDate(Date businessOperationDate) {
this.businessOperationDate = businessOperationDate;
}
public Version getVersion() {
return version;
}
public void setVersion(Version version) {
this.version = version;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
}
and the second one:
#Entity
public class Account extends BaseEntity{
private String number;
#OneToMany(mappedBy = "account", fetch = FetchType.EAGER)
#JsonBackReference
private List<Entry> entries;
#ManyToMany(mappedBy = "accounts")
private List<Project> projects;
public Account() {
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public List<Entry> getEntries() {
return entries;
}
public void setEntries(List<Entry> entries) {
this.entries = entries;
}
public List<Project> getProjects() {
return projects;
}
public void setProjects(List<Project> projects) {
this.projects = projects;
}
}
Here you can find part of result received from Http.GET request:
[{"id":1,"createdBy":null,"createdOn":null,"modifiedBy":null,"modifiedOn":null,"description":"pierwszy zapis","businessOperationDate":null,"version":null,"status":null,"account":{"id":1,"createdBy":null,"createdOn":null,"modifiedBy":null,"modifiedOn":null,"description":"pierwszy projekt","number":null,"entries":
[{"id":1,"createdBy":null,"createdOn":null,"modifiedBy":null,"modifiedOn":null,"description":"pierwszy zapis","businessOperationDate":null,"version":null,"status":null,"account":{"id":1,"createdBy":null,"createdOn":null,"modifiedBy":null,"modifiedOn":null,"description":"pierwszy projekt","number":null,"entries":
[{"id":1,"createdBy":null,"createdOn":null,"modifiedBy":null,"modifiedOn":null,"description":"pierwszy zapis","{"timestamp":1468778765328,"status":200,"error":"OK","exception":"org.springframework.http.converter.HttpMessageNotWritableException","message":
"Could not write content: Infinite recursion (StackOverflowError) (through reference chain: com.test.test2.core.dto.AccountDto[\"entries\"]->
java.util.ArrayList[0]->com.test.test2.core.dto.EntryDto[\"account\"]->com.test.test2.core.dto.AccountDto[\"entries\"]->
java.util.ArrayList[0]->com.test.test2.core.dto.EntryDto[\"account\"]->com.test.test2.core.dto.AccountDto[\"entries\"]->java.util.ArrayList[0]->com.test.test2.core.dto.EntryDto[\"account\"]->com.test.test2.core.dto.AccountDto[\"entries\"]->
java.util.ArrayList[0]->com.test.test2.core.dto.EntryDto[\"account\"]-
pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>test2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>test2</name>
<description>test2</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-security</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<version>9.4-1201-jdbc41</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- model mapper -->
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.4.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Please advise, what I am doing wrong. I wish to receive in a result only one level, e.g. calling getAll() for entry, I wish to receive all entries with information which account is related, and in opposite once calling getAll() for account.
i search more times for this error,but i can meet anything i get this error case and i correct it by adding the annotation #JsonIgnore in some relation mapping,
this is example
#ManyToMany(mappedBy = "accounts")
#JsonIgnore
private List<Project> projects;
Related
I am trying to build a basic CRUD application in Spring Boot.
Made a Model class as below :
Employee.java
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
#Entity
public class Employee implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, updatable = false)
private Long id;
private String name;
private String email;
private String jobTitle;
private String phone;
private String imageUrl;
#Column(nullable = false, updatable = false)
private String employeeCode;
public Employee() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getEmployeeCode() {
return employeeCode;
}
public void setEmployeeCode(String employeeCode) {
this.employeeCode = employeeCode;
}
public Employee(String name, String email, String jobTitle, String phone, String imageUrl, String employeeCode) {
this.name = name;
this.email = email;
this.jobTitle = jobTitle;
this.phone = phone;
this.imageUrl = imageUrl;
this.employeeCode = employeeCode;
}
#Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", email=" + email + ", jobTitle=" + jobTitle + ", phone="
+ phone + ", imageUrl=" + imageUrl + ", employeeCode=" + employeeCode + "]";
}
}
This is my application.properties file.
#MYSQL Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/employeemanager
spring.datasource.data-username=myusername
spring.datasource.password=mypassword
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
debug= true
My Pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.sood1</groupId>
<artifactId>EmployeeManager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>EmployeeManager</name>
<description>Employee Manager App</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Below is the logs for the error.
java.sql.SQLException: Access denied for user ''#'localhost' (using password: YES)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.23.jar:8.0.23]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.23.jar:8.0.23]
I tried establishing connection in sql command line interface with same credentials and it's working fine.
Can anyone help why this issue is coming up and what's the walk around for this.
Thank you in Advance
There is mistake in your configurations
spring.datasource.data-username=myusername change it to spring.datasource.username=myusername
I realize a very similar question was asked and closed because it wasn't specific enough and didn't specify outcomes. Closed Off Topic
Problem: JSON being returned from REST controller is empty. Verified data exists and is in the Iterable.
Expected Outcome: A JSON Array containing objects would be returned.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.codeexcursion</groupId>
<organization>
<name>Chris Lynch</name>
</organization>
<version>1.00.000</version>
<artifactId>work-tracking</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<name>Work Tracking</name>
<inceptionYear>2017</inceptionYear>
<developers>
<developer>
<id />
<name>Chris Lynch</name>
<email>chrislynch42#yahoo.com</email>
<timezone>-4</timezone>
<roles>
<role>Chief cook and bottle washer.</role>
</roles>
</developer>
</developers>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>1.5.10.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.13.1.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.codeexcursion.Application</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Entity
//Package and import Omitted
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long parentId;
private String title;
private String description;
protected Category() {
}
public Category(final String title, String description) {
this(0L, title, description);
}
public Category(Long parentId, final String title, String description) {
if (parentId == null) {
parentId = 0L;
}
if (title == null || title.trim().isEmpty()) {
throw new IllegalArgumentException("Title may not be null or empty.");
}
if (description == null) {
description = "";
}
this.parentId = parentId;
this.title = title;
this.description = description;
}
#Override
public String toString() {
return "id = " + id + "; parentId=" + parentId + "; title=" + title + "; description=" + description;
}
}
Resource
//Package and import Omitted
#Repository
public interface CategoryCRUD extends CrudRepository<Category, Long> {
List<Category> findByTitle(String title);
}
Rest Controller
//Package and import Omitted
#RestController
#RequestMapping("/categories")
public class CategoryController {
#Autowired
private CategoryCRUD categoryCRUD;
#RequestMapping(value = "", method = RequestMethod.GET)
public #ResponseBody Iterable<Category> list() {
System.out.println("findAll");
categoryCRUD.findAll().forEach((category) -> {
System.out.println("category=" + category);
});
return categoryCRUD.findAll();
}
private List<Category> findAll() {
final Iterable<Category> data = categoryCRUD.findAll();
List<Category> returnList = new ArrayList<>();
data.forEach(returnList::add);
return returnList;
}
}
I found an answer that was hinted on the closed post but wasn't detailed. I needed to add getters to my entity. I expected JPA/Spring to automagically add the getters and setters. The below fixed my problem.
Entity
//Package and imports omitted
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long parentId;
private String title;
private String description;
protected Category() {
}
public Category(final String title, String description) {
this(0L, title, description);
}
public Category(Long parentId, final String title, String description) {
if (parentId == null) {
parentId = 0L;
}
if (title == null || title.trim().isEmpty()) {
throw new IllegalArgumentException("Title may not be null or empty.");
}
if (description == null) {
description = "";
}
this.parentId = parentId;
this.title = title;
this.description = description;
}
#Override
public String toString() {
return "id = " + id + "; parentId=" + parentId + "; title=" + title + "; description=" + description;
}
public Long getId() {
return id;
}
public Long getParentId() {
return parentId;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
}
Better answers are welcome.
In my case, the getters to the fields of the entity were not public.
Making them public fixed the issue for me.
You have to include the lombok dependency in your pom.xml file and you have to setup the lombok jar in the IDE you are using (Can be Intellij or Eclipse). if you want to use the annotations #Data, it automatically generates the getters, setters and toString() method inside a Java Bean or Pojo class.
You can use #Getter, #Setter, #AllArgsConstructor, #NoArgsConstructor javadoc annotation from lombok will generate the getters and setters and constructors for your fields.
Please have a look at this http://www.baeldung.com/intro-to-project-lombok for more information.
Thanks!
I am developing an Spring Boot app based on Spring Data Rest(which uses Hibernate underneath) and mySQL database. This app is incapable to populate the foreign keys of the referenced entries(because I expect Hibernate does it for me).
Entities:
#Entity
public class Producto {
private Integer id;
private String nombre;
private List<Formato> listaFormatos;
public Producto() {
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
#OneToMany(mappedBy = "producto", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
public List<Formato> getListaFormatos() {
return listaFormatos;
}
public void setListaFormatos(List<Formato> listaFormatos) {
this.listaFormatos = listaFormatos;
}
}
#Entity
public class Formato {
private Integer id;
private Integer cantidad;
private String unidadMedida;
private Producto producto;
public Formato() {
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#ManyToOne
#JoinColumn(name = "producto_id", referencedColumnName = "id")
public Producto getProducto() {
return producto;
}
public void setProducto(Producto producto) {
this.producto = producto;
}
public Integer getCantidad() {
return cantidad;
}
public void setCantidad(Integer cantidad) {
this.cantidad = cantidad;
}
public String getUnidadMedida() {
return unidadMedida;
}
public void setUnidadMedida(String unidadMedida) {
this.unidadMedida = unidadMedida;
}
}
Repository:
public interface ProductoRepository extends CrudRepository<Producto, Integer> {
}
application.properties
spring.datasource.url = jdbc:mysql://localhost:3306/x1
spring.datasource.username = x2
spring.datasource.password = x3
spring.jpa.show-sql=true
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=create-drop
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Let's say that the request I am sending in JSON format looks like this:
{"nombre": "x",
"listaFormatos": [
{"cantidad": 1,
"unidadMedida":"kg"},
{"cantidad": 2,
"unidadMedida":"g"}
]
}
The output of all this is that I persist an 'producto' and 2 'formato' without foreign key. Because of that, I believe I cannot bring a producto with its formats
Does anyone know why the foreign key is not being propagated?
Empty FKs usually happening when you are not setting the back references.
Consider the following:
Parent p = new Parent();
...
Child c = new Child();
...
p.setChild(c);
c.setParent(p); // this is the line you are probably missing
Of course, you can put this logic into the Parent#setChild method as well.
I am trying to add #NotNull constraint into my Person object but I still can #POST a new Person with a null email. I am using Spring boot rest with MongoDB.
Entity class:
import javax.validation.constraints.NotNull;
public class Person {
#Id
private String id;
private String username;
private String password;
#NotNull // <-- Not working
private String email;
// getters & setters
}
Repository class:
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {
}
Application class:
#SpringBootApplication
public class TalentPoolApplication {
public static void main(String[] args) {
SpringApplication.run(TalentPoolApplication.class, args);
}
}
pom.xml
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
...
When I #POST a new object via Postman like:
{
"username": "deadpool",
"email": null
}
I still get STATUS 201 created with this payload:
{
"username": "deadpool",
"password": null,
"email": null
....
....
}
I had the same problem, but just enabling validation didn't work for me, this did work with both JPA and MongoDb to save anyone else spending ages on this. Not only does this get validation working but I get a nice restful 400 error rather than the default 500.
Had to add this to my build.gradle dependencies
compile('org.hibernate:hibernate-validator:4.2.0.Final')
and this config class
#Configuration
public class CustomRepositoryRestConfigurerAdapter extends RepositoryRestConfigurerAdapter {
#Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
validatingListener.addValidator("afterCreate", validator());
validatingListener.addValidator("beforeCreate", validator());
validatingListener.addValidator("afterSave", validator());
validatingListener.addValidator("beforeSave", validator());
}
}
i found it better to make my own version of #NotNull annotation which validates empty string as well.
#Documented
#Constraint(validatedBy = NotEmptyValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface NotEmpty {
String message() default "{validator.notEmpty}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, Object> {
#Override
public void initialize(NotEmpty notEmpty) { }
#Override
public boolean isValid(Object obj, ConstraintValidatorContext cxt) {
return obj != null && !obj.toString().trim().equals("");
}
}
You can either use the following code for validating
#Configuration
#Import(value = MongoAutoConfiguration.class)
public class DatabaseConfiguration extends AbstractMongoConfiguration
{
#Resource
private Mongo mongo;
#Resource
private MongoProperties mongoProperties;
#Bean
public ValidatingMongoEventListener validatingMongoEventListener() {
return new ValidatingMongoEventListener(validator());
}
#Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Override
public Mongo mongo() throws Exception {
return mongo;
}
}
Normally, the #RestRepository will resolve into a controller than handles validation by itself, except if you Override the default behavior or it by including some #HandleBeforeSave, #HandleBeforeCreate, ... into your code.
A solution is to remove the #HandleBeforeSave, #HandleBeforeCreate, ...
and then spring will handle the validation again.
Or if you want to keep them, you can provide a handler for any object validation like this:
#Component
#RepositoryEventHandler
public class EntityRepositoryEventHandler {
#Autowired
private Validator validator;
#HandleBeforeSave
#HandleBeforeCreate
public void validate(Object o) {
Set<ConstraintViolation<Object>> violations = this.validator.validate(o);
if (!violations.isEmpty()) {
ConstraintViolation<Object> violation = violations.iterator().next();
// do whatever your want here as you got a constraint violation !
throw new RuntimeException();
}
}
}
I would like to know if anyone else has used hibernate-envers with mysql in a Spring-boot application using the interface CrudRepository and has had trouble deleting entities with collections. I put together an example application with a test that demonstrates the exception that is generated.
The example can be cloned from https://github.com/LindesRoets/test-delete.git
You will need mysql running with a database called test_delete
CREATE DATABASE IF NOT EXISTS `test_delete` DEFAULT CHARACTER SET utf8;
Run the test
mvn test
You should see the following exception:
2015-08-11 21:36:28.725 ERROR 3855 --- [ main] org.hibernate.AssertionFailure : HHH000099: an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): java.lang.NullPointerException
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 4.926 sec <<< FAILURE! - in com.dcp.test.AuthorRepositoryTest
testAuthorCRUD(com.dcp.test.AuthorRepositoryTest) Time elapsed: 0.163 sec <<< ERROR!
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
at org.hibernate.engine.internal.StatefulPersistenceContext.getLoadedCollectionOwnerOrNull(StatefulPersistenceContext.java:756)
at org.hibernate.event.spi.AbstractCollectionEvent.getLoadedOwnerOrNull(AbstractCollectionEvent.java:75)
at org.hibernate.event.spi.InitializeCollectionEvent.<init>(InitializeCollectionEvent.java:36)
at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:1931)
at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:558)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:260)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:554)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:142)
at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:294)
at java.util.AbstractCollection.addAll(AbstractCollection.java:343)
at org.hibernate.envers.internal.entities.mapper.relation.AbstractCollectionMapper.mapCollectionChanges(AbstractCollectionMapper.java:162)
at org.hibernate.envers.internal.entities.mapper.relation.AbstractCollectionMapper.mapModifiedFlagsToMapFromEntity(AbstractCollectionMapper.java:212)
at org.hibernate.envers.internal.entities.mapper.MultiPropertyMapper.map(MultiPropertyMapper.java:105)
at org.hibernate.envers.internal.synchronization.work.DelWorkUnit.generateData(DelWorkUnit.java:66)
at org.hibernate.envers.internal.synchronization.work.AbstractAuditWorkUnit.perform(AbstractAuditWorkUnit.java:76)
at org.hibernate.envers.internal.synchronization.AuditProcess.executeInSession(AuditProcess.java:119)
Below is all the source code as it appears in the sample application.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>test-delete</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.5.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>com.test.Application</start-class>
<java.version>1.8</java.version>
<spring-boot-version>1.2.5.RELEASE</spring-boot-version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<includes>
<include>**/*Test*.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.3</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot-version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
</dependencies>
</project>
application.properties
spring.datasource.url = jdbc:mysql://localhost:3306/test_delete
spring.datasource.username =
spring.datasource.password =
# Specify the DBMS
spring.jpa.database = MYSQL
spring.datasource.driverClassName = com.mysql.jdbc.Driver
# Show or not log for each sql query
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto = create
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
#envers
spring.jpa.properties.org.hibernate.envers.global_with_modified_flag=true
spring.jpa.properties.org.hibernate.envers.store_data_at_delete=true
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
BaseEntity.java
#MappedSuperclass
#Audited
public abstract class BaseEntity implements Serializable {
#GeneratedValue(generator = "guid")
#GenericGenerator(name = "guid", strategy = "guid")
#Column(columnDefinition = "CHAR(36)")
#Id
protected String id;
#Temporal(TemporalType.TIMESTAMP)
protected Date dateCreated = new Date();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getDateCreated() {
return dateCreated;
}
public void setDateCreated(Date dateCreated) {
this.dateCreated = dateCreated;
}
private static final long serialVersionUID = -8371628134270930829L;
}
Author.java
#Entity
#Audited
public class Author extends BaseEntity{
private String email;
private String name;
#OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}
}
Book.java
#Entity
#Audited
public class Book extends BaseEntity {
private String title;
private String genre;
#ManyToOne
private Author author;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getGenre() {
return genre;
}
public void setGenre(String genre) {
this.genre = genre;
}
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
}
AuthorRepository.java
#Transactional
public interface AuthorRepository extends CrudRepository<Author, String> {
}
AuthorRepositoryTest.java
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class AuthorRepositoryTest {
#Autowired
private AuthorRepository authorRepo;
#Test
public void testAuthorCRUD() {
Author author = new Author();
author.setName("Test Name");
author.setEmail("test#mail.com");
Author savedAuthor = authorRepo.save(author);
Assert.assertNotNull(savedAuthor);
authorRepo.delete(savedAuthor);
}
}
If you comment out the #Audited from the entity classes Book, Author and BaseEntity the test pass just fine.
Does anyone know how to make the delete functionality work with envers?
There are two ways that I found through trial and error to make the delete operation work as specified in the problem.
You can either specify the collection to load eagerly
#OneToMany(mappedBy = "author", fetch = FetchType.EAGER)
Or you can cascade the delete operation
#OneToMany(mappedBy = "author", cascade = CascadeType.REMOVE)
This is the minimum configuration that you will need to make the test pass as specified in the problem.
Had the same issue, fixed by doing an explicit join to the relationship in question, also I think it's a common thing to delete child relationships first as well so it might be a thing you will have to do anyways.
I think in the context of envers you have to use repository delete methods to be able to save the revisions on delete.