I found a bug related to Spring Boot and JPA when creating tables. When there is a serialized entity associated with a built-in serialized class, which in turn has another built-in serialized class, and finally, this class has a serialized entity, Spring Boot fails to start the server, and JPA doesn't create the database tables. Instead, it gets stuck at `HikariPool-1: Start Completed.
Image here: (https://github.com/spring-projects/spring-boot/assets/97984278/67b8353c-d805-4e94-ab1b-d71ab0b45456)
I talked to a member of the spring boot team (https://github.com/spring-projects/spring-boot/issues/38701) and he told me that maybe it's a bug related to spring data jpa, hibernate or hikari and so he asked me to create a question here.
The problem seems to be with this serial association of entities with embedded classes and entities. If you would like to run and check the issue to verify whether this is indeed a problem with either Spring Boot (which fails to start because of this) or JPA or Hikari, I would appreciate it.
The example is a little long, but it is to ensure that you will test it the same way I tested it. (I tried to reduce it a lot to make it simpler).
<?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>3.2.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.test</groupId>
<artifactId>test-api</artifactId>
<version>1.0</version>
<name>test-backend</name>
<description>Test</description>
<properties>
<java.version>18</java.version>
</properties>
<dependencies>
<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-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-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>
# MySQL database configuration
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/bug_db?allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true&useTimezone=true&serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql=true
version: '3.1'
services:
db:
image: mysql:8.0.32
restart: always
environment:
MYSQL_DATABASE: bug_db
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
@Entity
@Table(name = "accounts", uniqueConstraints = { @UniqueConstraint(columnNames = { "username" }) })
public final class Account implements UserDetails {
/** The serialVersionUID. */
private static final long serialVersionUID = 221625420706334299L;
/** The unique identifier for the account. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** The user name for authentication. */
@Column(nullable = false, unique = true)
@NotBlank(message = "The username cannot be blank")
private String username;
/**
* The password for authentication. */
@Column(name = "password", nullable = false)
@JsonIgnore
@NotBlank(message = "The password cannot be blank")
private String password;
/** The information of the account holder. */
@Embedded
@Valid
private AccountHolderInformation holderInformation;
/** Indicates whether it is account non expired. False by default. */
@Column(columnDefinition = "boolean default false", nullable = false)
private boolean isAccountNonExpired;
/** Indicates whether it is account non locked. False by default. */
@Column(columnDefinition = "boolean default false", nullable = false)
private boolean isAccountNonLocked;
/** Indicates whether it is enabled. False by default. */
@Column(columnDefinition = "boolean default false", nullable = false)
private boolean isEnabled;
/** The role of the account in the system. */
@Column(name = "role", nullable = false)
@Enumerated(EnumType.STRING)
private Role role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public AccountHolderInformation getHolderInformation() {
return holderInformation;
}
public void setHolderInformation(AccountHolderInformation holderInformation) {
this.holderInformation = holderInformation;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (this.role == Role.ROLE_ADMIN) {
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
} else {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
public void setAccountNonExpired(boolean isAccountNonExpired) {
this.isAccountNonExpired = isAccountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
public void setAccountNonLocked(boolean isAccountNonLocked) {
this.isAccountNonLocked = isAccountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
}
public interface AccountRepository extends JpaRepository<Account, Long>{
Optional<Account> findByUsername(String username);
}
public enum Role {
ROLE_ADMIN("admin"),
ROLE_USER("user");
private final String key;
private Role(String key) {
this.key = key;
}
public String getRole() {
return key;
}
}
@Embeddable
public final class AccountHolderInformation implements Serializable {
/**
* The serialVersionUID.
*/
private static final long serialVersionUID = 4089056018657825205L;
/** The first name of the account holder. */
@Column(nullable = false)
@NotBlank(message = "The name cannot be blank")
private String name;
/** (Optional) The last name or surname of the account holder. */
@Column
@Length
private String surname;
/** The security information of the account holder. */
@Embedded
@Valid
private AccountHolderSecurityInformation securityInformation;
//getters and setters
}
@Embeddable
public final class AccountHolderSecurityInformation implements Serializable {
/**
* The serialVersionUID.
*/
private static final long serialVersionUID = 3585858950258340583L;
/** The first security question to confirm the identity of an account holder. */
@JsonIgnore
@OneToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, mappedBy = "account")
private AccountSecurityQuestion securityQuestionOne;
//getters and setters
}
@Entity
@Table(name = "accounts_security_questions")
public final class AccountSecurityQuestion implements Serializable {
/** The serialVersionUID. */
private static final long serialVersionUID = -8188615055579913942L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JoinColumn(name = "account_id", nullable = false)
@JsonIgnore
@ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false)
private Account account;
@ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "security_question_id", nullable = false)
private SecurityQuestion securityQuestion;
@JsonIgnore
@NotBlank(message = "The answer cannot be blank")
private String answer;
//getters and setters
}
public interface AccountSecurityQuestionRepository extends JpaRepository<AccountSecurityQuestion, Long> {
}
@Entity
@Table(name = "security_questions")
public final class SecurityQuestion implements Serializable {
/** The serialVersionUID. */
private static final long serialVersionUID = -6788149456783476682L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@NotBlank(message = "The question cannot be blank")
private String question;
//getters and setters
}
public interface SecurityQuestionRepository extends JpaRepository<SecurityQuestion, Long> {
}
I've tried various solutions, such as cleaning the Maven repository, running locally, using Docker, deleting and recreating the database, and even renaming the database. Strangely, it worked only when I removed the association from one embedded class to another.`
I solved the problem by removing the associations of the serialized built-in classes with the serialized entity and it worked.
Apparently, JPA or Hibernate encounters difficulties starting the table creation process when it encounters a serialized entity with serialized embedded classes, and these embedded classes, in turn, involve serialized entities. As a result, Spring Boot gets stuck and fails to start