英文:
Cascade on bidirectional @OneToOne with @MapsId
问题
我使用的是Spring Boot 2.1.1.RELEASE 和 Hibernate 5.3.7.FINAL 版本。
规则是,用户可以没有电话(user 中的 phone 可为空),但是电话不能没有用户(phone 中的 user 不可为空)。
实体类:
@Entity
public class Phone {
   @Id
   private Long id;
   @OneToOne
   @MapsId
   @JoinColumn(name = "id")
   private User user;
   public Long getId() {
       return id;
   }
   public void setId(final Long id) {
       this.id = id;
   }
   public User getUser() {
      return user;
   }
   public void setUser(final User user) {
      this.user = user;
   }
}
@Entity
public class User {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
   @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
   private Phone phone;
   public Long getId() {
       return id;
   }
   public void setId(final Long id) {
       this.id = id;
   }
   public Phone getPhone() {
       return phone;
   }
   public void setPhone(final Phone phone) {
       this.phone = phone;
   }
}
控制器:
@RestController
@RequestMapping
public class UserController {
   private final UserService userService;
   public UserController(final UserService userService) {
       this.userService = userService;
   }
   @GetMapping("/demo")
   public void createUserAndAddPhone() {
       final User user = new User();
       userService.save(user);
       final Phone phone = new Phone();
       phone.setUser(user);
       user.setPhone(phone);
       userService.update(user);
   }
}
仓库:
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
}
服务:
@Service
public class UserService {
   private final UserRepository userRepository;
   public UserService(final UserRepository userRepository) {
       this.userRepository = userRepository;
   }
   @Transactional
   public void save(final User user) {
       userRepository.save(user);
   }
   @Transactional
   public void update(final User user) {
       userRepository.save(user);
   }
}
表结构:
CREATE TABLE `phone` (
   `id` bigint(20) NOT NULL,
   PRIMARY KEY (`id`)
);
CREATE TABLE `user` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
)
应用配置 application.yml:
spring:
   datasource:
      url: jdbc:mysql://localhost:3308/demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
   jpa:
      database-platform: org.hibernate.dialect.MySQL5Dialect
      hibernate:
         ddl-auto: validate
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.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.code.findbugs</groupId>
            <artifactId>jsr305</artifactId>
            <version>2.0.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
调用 GET http://localhost:8080/demo 时出现错误:
org.hibernate.id.IdentifierGenerationException: attempted to assign id
from null one-to-one property [com.example.demo.Phone.user]
当我注释掉 userService.save(user); 时,代码可以正常运行并生成以下 SQL:
insert into `user` 
values ( )
-- Generated identifier: 13, using strategy: org.hibernate.id.ForeignGenerator
                
insert into `phone` (`id`)  values (?)
-- Binding parameter [1] as [BIGINT] - [13]
但是如果先持久化 user,然后再更新,就会出现问题(引发上述异常)。
英文:
I work on spring boot 2.1.1.RELEASE, hibernate 5.3.7.FINAL
Rules are, that user can have no phone (phone is nullable in user) but, phone can't exist without a user (user is not null in phone).
Entities:
@Entity
public class Phone {
    
   @Id
   private Long id;
    
   @OneToOne
   @MapsId
   @JoinColumn(name = "id")
   private User user;
    
   public Long getId() {
       return id;
   }
   public void setId(final Long id) {
       this.id = id;
   }
    
   public User getUser() {
      return user;
   }
   public void setUser(final User user) {
      this.user = user;
   }
}
        
@Entity
public class User {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
    
   @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
   private Phone phone;
    
   public Long getId() {
       return id;
   }
   public void setId(final Long id) {
       this.id = id;
   }
    
   public Phone getPhone() {
       return phone;
   }
   public void setPhone(final Phone phone) {
       this.phone = phone;
   }
}
Controller:
@RestController
@RequestMapping
public class UserController {
    
   private final UserService userService;
    
   public UserController(final UserService userService) {
       this.userService = userService;
   }
    
   @GetMapping("/demo")
   public void createUserAndAddPhone() {
       final User user = new User();
       userService.save(user);
       final Phone phone = new Phone();
    
       phone.setUser(user);
       user.setPhone(phone);
       userService.update(user);
   }
}
Repository:
public interface UserRepository  extends PagingAndSortingRepository<User, Long> {
}
Serivce:
@Service
public class UserService {
    
   private final UserRepository userRepository;
    
   public UserService(final UserRepository userRepository) {
       this.userRepository = userRepository;
   }
    
   @Transactional
   public void save(final User user) {
       userRepository.save(user);
   }
    
   @Transactional
   public void update(final User user) {
       userRepository.save(user);
   }
}
Tables:
CREATE TABLE `phone` (
   `id` bigint(20) NOT NULL,
   PRIMARY KEY (`id`)
);
    
CREATE TABLE `user` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
)
application yml:
spring:
   datasource:
      url: jdbc:mysql://localhost:3308/demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
   jpa:
      database-platform: org.hibernate.dialect.MySQL5Dialect
      hibernate:
         ddl-auto: validate
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.1.1.RELEASE</version>
   		<relativePath/> <!-- lookup parent from repository -->
   	</parent>
   	<groupId>com.example</groupId>
   	<artifactId>demo</artifactId>
   	<version>0.0.1-SNAPSHOT</version>
   	<name>demo</name>
   	<description>Demo project for Spring Boot</description>
    
   	<properties>
   		<java.version>1.8</java.version>
   	</properties>
    
   	<dependencies>
   		<dependency>
   			<groupId>org.springframework.boot</groupId>
   			<artifactId>spring-boot-starter</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-web</artifactId>
   		</dependency>
   		<dependency>
   			<groupId>com.google.code.findbugs</groupId>
   			<artifactId>jsr305</artifactId>
   			<version>2.0.1</version>
   		</dependency>
   		<dependency>
   			<groupId>mysql</groupId>
   			<artifactId>mysql-connector-java</artifactId>
   			<version>5.1.46</version>
  			<scope>runtime</scope>
   		</dependency>
   	</dependencies>
    
   	<build>
   		<plugins>
   			<plugin>
   				<groupId>org.springframework.boot</groupId>
   				<artifactId>spring-boot-maven-plugin</artifactId>
   			</plugin>
   		</plugins>
   	</build>
</project>
Call GET http://localhost:8080/demo gives me an error:
> org.hibernate.id.IdentifierGenerationException: attempted to assign id
> from null one-to-one property [com.example.demo.Phone.user]
When I comment out userService.save(user);, it works and generates:
insert into `user` 
values ( )
-- Generated identifier: 13, using strategy: org.hibernate.id.ForeignGenerator
                
insert into `phone` (`id`)  values (?)
-- Binding parameter [1] as [BIGINT] - [13]
but if the user is persisted and then updated, it doesn't work (raises the above exception)
答案1
得分: 2
啊,最终,这是一个 Hibernate 的 bug(HHH-12436)。
通过以下用例,在纯 Hibernate 应用程序中可以重现这个问题:
Session session = sessionFactory.openSession();
Transaction tr1 = session.beginTransaction();
User user = new User();
session.persist(user);
tr1.commit();
Transaction tr2 = session.beginTransaction();
Phone newPhone = new Phone();
user.setPhone(newPhone);
newPhone.setUser(user);
session.merge(user);
tr2.commit();
session.close();
如你可以从上面的链接看到,这个问题在 Hibernate 5.4 分支中已经修复。
附注:我能够在最新的 5.3 版本(5.3.18.Final)中复现这个问题。
英文:
Ah, in the end, this is a hibernate bug (HHH-12436).
It is reproducible in a pure hibernate application by the following use case:
Session session = sessionFactory.openSession();
Transaction tr1 = session.beginTransaction();
User user = new User();
session.persist(user);
tr1.commit();
Transaction tr2 = session.beginTransaction();
Phone newPhone = new Phone();
user.setPhone(newPhone);
newPhone.setUser(user);
session.merge(user);
tr2.commit();
session.close();
As you can see from the above link it is fixed in hibernate 5.4 branch.
P.S. I was able to reproduce the problem in the latest 5.3 version (5.3.18.Final)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论