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 Complete.
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.
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.
- Spring version: 3.2.0 or less;
- MySQL 8.0.32 or more;
- Java 18 or more;
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).
pom.xml
<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>
application.properties:
# 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
docker-compose.yml
version: '3.1'
services:
db:
image: mysql:8.0.32
restart: always
environment:
MYSQL_DATABASE: bug_db
MYSQL_ROOT_PASSWORD: root
ports:
- "3306:3306"
Account.java
@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;
}
}
AccountRepository.java
public interface AccountRepository extends JpaRepository<Account, Long>{
Optional<Account> findByUsername(String username);
}
Role.java
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;
}
}
AccountHolderInformation.java
@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
}
AccountHolderSecurityInformation.java
@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
}
AccountSecurityQuestion.java
@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
}
AccountSecurityQuestionRepository.java
public interface AccountSecurityQuestionRepository extends JpaRepository<AccountSecurityQuestion, Long> {
}
SecurityQuestion.java
@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
}
SecurityQuestionRepository.java
public interface SecurityQuestionRepository extends JpaRepository<SecurityQuestion, Long> {
}
Comment From: AlefMemTav
Sorry if the example is long. I don't have much more time to spend on this, I solved the bug by removing the association. Please, if you want to test it the way I did, I would be grateful, but I don't have time to discuss this further.
Comment From: philwebb
This is unlikely to be a bug in Spring Boot itself. It's possible you've found a problem in Spring Data JPA, Hibernate or Hikari but we're not going to be able to help with that in this issue tracker. I would suggest starting with a question on stackoverflow.com and including a link to a project on GitHub so that folks can just do git clone to get the code.
Comment From: AlefMemTav
@philwebb Thank you, Phil.