JPA / Hibernate One to One Mapping Example with Spring Boot

来源

作者主页链接

说明

Hibernate是JAVA中最受欢迎的ORM(对象关系映射)工具。它实现了JPA规范并且被业界广泛使用。

Hibernate映射数据库表到应用中的实体类。

你可以像在数据库中定义表关系那样,在你的应用中定义实体类之间的关系。

这篇文章会告诉你如何定义 one-to-one关系在两个实体类之间。

让我们考虑一个应用程序,它存储有关其用户的大量信息,以便它可以为他们提供个性化的体验。
在这一情况下,存储主要信息(名字,邮箱,密码)的表和次要信息(住址,性别等)的表分开就很有意义。我们假设主表叫USERS,副表叫USER_PROFILES,他们之间有一对一的关系。

就像下面的数据库schema:
表映射
两个表有一对一的关系(一个user数据对应一个user_profile数据),通过在user_profiles表加外键user_id来映射。

接下来,我们创建一个项目来演示如何通过JPA和HIBERNATE来实现一对一关系映射。
想直接看代码的去这one-to-one-demo

创建项目

使用Spring Initializr工具生成项目。

  1. 进入 http://start.spring.io
  2. 切换到全版本链接模式
  3. 改变保命为com.example.jpa
  4. 选择Web,JPA,Mysql依赖
  5. 点击生成项目

配置数据库和日志等级

因为我们使用MYSQL数据库,我们需要配置数据库URL,username,password。打开 src/main/resources/application.properties file

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:mysql://localhost:3306/jpa_one_to_one_demo?useSSL=false
spring.datasource.username=root
spring.datasource.password=root

# Hibernate

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

属性 spring.jpa.hibernate.ddl-auto = update 使得当你的应用启动时,会自动更新/创建数据库表(根据你的实体类)
logging propertites 将帮助我们debugSQL信息。

设置数据库链接为你的本地链接。同时创建一个 jpa_one_to_one_demo 的数据库。

定义实体类

定义实体类,这些类会被映射到我们前面说的数据库的表。

1. User Entity

创建包model,创建User 类在model包内。

package com.example.jpa.model;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.validation.constraints.Email;
import java.io.Serializable;

@Entity
@Table(name = "users")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Size(max = 65)
    @Column(name = "first_name")
    private String firstName;

    @Size(max = 65)
    @Column(name = "last_name")
    private String lastName;

    @NotNull
    @Email
    @Size(max = 100)
    @Column(unique = true)
    private String email;

    @NotNull
    @Size(max = 128)
    private String password;

    @OneToOne(fetch = FetchType.LAZY,
            cascade =  CascadeType.ALL,
            mappedBy = "user")
    private UserProfile userProfile;

    // Hibernate requires a no-arg constructor
    public User() {

    }

    public User(String firstName, String lastName, String email, String password) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.password = password;
    }

    // Getters and Setters (Omitted for brevity)
}

2. Gender Enum

Gender枚举在Use’r’Profile类中存储性别时使用。

package com.example.jpa.model;

public enum Gender {
    MALE,
    FEMALE
}

3. UserProfile Entity

最后,创建UerProfille类。

package com.example.jpa.model;

import javax.persistence.*;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "user_profiles")
public class UserProfile implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "phone_number")
    @Size(max = 15)
    private String phoneNumber;

    @Enumerated(EnumType.STRING)
    @Column(length = 10)
    private Gender gender;

    @Temporal(TemporalType.DATE)
    @Column(name = "dob")
    private Date dateOfBirth;

    @Size(max = 100)
    private String address1;

    @Size(max = 100)
    private String address2;

    @Size(max = 100)
    private String street;

    @Size(max = 100)
    private String city;

    @Size(max = 100)
    private String state;

    @Size(max = 100)
    private String country;

    @Column(name = "zip_code")
    @Size(max = 32)
    private String zipCode;

    @OneToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    public UserProfile() {

    }

    public UserProfile(String phoneNumber, Gender gender, Date dateOfBirth, 
                       String address1, String address2, String street, String city, 
                       String state, String country, String zipCode) {
        this.phoneNumber = phoneNumber;
        this.gender = gender;
        this.dateOfBirth = dateOfBirth;
        this.address1 = address1;
        this.address2 = address2;
        this.street = street;
        this.city = city;
        this.state = state;
        this.country = country;
        this.zipCode = zipCode;
    }

    // Getters and Setters (Omitted for brevity)
}

一个一对一关系通过JPA的@OneToOne注解来定义。他有几个参数-

fetch = FetchType.LAZY - 懒加载,直到使用时才从数据库中提取信息。

cascade = CascadeType.ALL - 级联,无论什么时候更新或删除一个User实,同时更新/删除对应的UserProfiles。

mappedBy = “user” - User实体中的UserProfile成员变量有这一个属性。意思是这个一对一关系不由User类维护,由UserProfile中的user成员变量来维护。hibernate看到这个配置时,会去UserProfile中找到user字段,并使用字段上的配置来维护一对一关系。

自言
我们在User类中存储了一个字段userProfile,而这个UserProfile也是一个类,这在编程语言中非常正常,但在关系型数据库中这是做不到的(你能想象一个表的某个字段存储了另一个表的一行数据?),存储做不到,映射还是可以的(user_profile表中有一个user_id的字段,是不是就关联上了)。
再说hibernate的处理过程,首先Hibernate看见@OneToOne注解时,就知道他要拿多个表的数据来组织数据(这里就是User中有UserProfile),即当生成一个user实体时,自然要填充它的userProfile字段,于是要去UserProfile表中拿数据,怎么拿?根据mappedBy="user"找到UserProfile类中的user字段上的定义,即@JoinColumn(name = “user_id”, nullable = false)
通过数据库中user_id这个字段来找。
等价于

 select * 
 from user
 left join user_profile 
 on
    user.id = user_profile.user_id

这样返回的user实体就包含了user_profile的信息。
不过这里hibernate是通过两个sql做的,没有用join。

在一个双向关系中,我们在两个实体都加了@OneToOne 注解,但是只有一个实体是这一关系的拥有着。大多数情况下,子实体(这里是UserProfile)是拥有者。
拥有者包含一个@JoinColumn注解指明外键列,非拥有者那一边有一个mappedBy属性表明映射到其他实体。
自言:为什么是子实体来维护这一关系?因为是子跟随父实体。当User删除或更新时,那作为子的UserProfile要跟着更新或删除,即这段跟随关系是由子实体维护的。外键加在子实体上也是相同的道理,子实体想要自己删除时是不可以的,因为父实体中还存在对应的数据)

Defining the Repositories

最后,定义仓库接口来访问数据库。

我们继承JpaRepository接口就可以了,JPA已经有叫SimpleJpaRepository的实现类,应用启动时它会自动代理。

1. UserRepository

package com.example.jpa.repository;

import com.example.jpa.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

}

2. UserProfileRepository

package com.example.jpa.repository;

import com.example.jpa.model.UserProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {

}

写代码

package com.example.jpa;

import com.example.jpa.model.Gender;
import com.example.jpa.model.User;
import com.example.jpa.model.UserProfile;
import com.example.jpa.repository.UserRepository;
import com.example.jpa.repository.UserProfileRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Calendar;

@SpringBootApplication
public class JpaOneToOneDemoApplication implements CommandLineRunner {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserProfileRepository userProfileRepository;

    public static void main(String[] args) {
        SpringApplication.run(JpaOneToOneDemoApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        // Clean up database tables
        userProfileRepository.deleteAllInBatch();
        userRepository.deleteAllInBatch();

        //=========================================

        // Create a User instance
        User user = new User("Rajeev", "Singh", "rajeev@callicoder.com",
                "MY_SUPER_SECRET_PASSWORD");

        Calendar dateOfBirth = Calendar.getInstance();
        dateOfBirth.set(1992, 7, 21);

        // Create a UserProfile instance
        UserProfile userProfile = new UserProfile("+91-8197882053", Gender.MALE, dateOfBirth.getTime(),
                "747", "2nd Cross", "Golf View Road, Kodihalli", "Bangalore",
                "Karnataka", "India", "560008");


        // Set child reference(userProfile) in parent entity(user)
        user.setUserProfile(userProfile);

        // Set parent reference(user) in child entity(userProfile)
        userProfile.setUser(user);

        // Save Parent Reference (which will save the child as well)
        userRepository.save(user);

        //=========================================
    }
}

实现CommandLineRunner 接口可以在启动时运行一些代码。

我们先清除表数据,再插入一个User和对应的UserProfile。

你可以检查debug日志。

org.hibernate.SQL : delete from user_profiles
org.hibernate.SQL : delete from users
org.hibernate.SQL : insert into users (email, first_name, last_name, password) values (?, ?, ?, ?)
org.hibernate.SQL : insert into user_profiles (address1, address2, city, country, dob, gender, phone_number, state, street, user_id, zip_code) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
<think>首先,用户在询问Spring Boot中的映射文件定义循环引用。用户的问题是关于在Spring Boot的映射文件中定义循环关系或循环引用。这似乎涉及数据库映射或对象关系映射(ORM),可能是在MyBatis或JPA的上下文中。 用户提到“mapping 文件”,在Spring Boot中,映射文件通常指的是MyBatis的XML映射文件或Hibernate的映射文件。用户指定了“Spring Boot mapping 文件”,所以很可能是在MyBatis的上下文中,因为MyBatis使用XML映射文件来定义SQL语句和结果映射。 用户的问题是关于“循环引用”,意思是循环关系,比如两个实体相互引用,例如,一个Order实体有一个Customer引用,而Customer实体有一个Order引用。这可能导致无限递归问题,在序列化或映射时需要处理。 在Spring Boot中,处理循环引用通常涉及ORM框架。参考提供的引用: - 引用[1]:谈论Spring Boot启动类。 - 引用[2]:提到@MapperScan注解,用于扫描MyBatis的mapper接口。 - 引用[3]:关于WebMvcConfigurer处理URL映射。 - 引用[4]和[5]:关于Elasticsearch映射和分词器。 这些引用似乎不直接相关到用户的问题。引用[2]提到了@MapperScan,这暗示MyBatis,但用户的问题是关于循环引用。引用[4]和[5]是关于Elasticsearch的,不是数据库映射。 用户的问题:“我想在Spring Boot中如何在映射文件中定义循环关系 请问Spring Boot mapping 文件定义循环引用” 所以,我需要基于ORM框架,如MyBatis或JPA,来回答。 在MyBatis中,映射文件是XML文件,用于定义SQL查询和结果映射。处理循环引用时,MyBatis提供了<association>和<collection>元素来处理关系,但对于循环引用,需要小心避免无限递归。 在JPA中(如使用Hibernate),可以使用@OneToOne、@OneToMany等注解,但循环引用可能导致序列化问题,比如在使用Jackson时出现StackOverflowError。 用户指定了“mapping 文件”,所以更可能是在MyBatis上下文中。 我的回答应该: 1. 使用中文回答。 2. 结构清晰,逐步解决问题。 3. 确保真实可靠。 4. 对于数学表达式,使用$...$或$$...$$格式,但这个问题可能不涉及数学,所以可能不需要。 5. 生成相关问题。 6. 在回答中引用的段落末尾添加引用标识,但用户提供的引用可能不直接相关,所以我可以参考它们或忽略。 用户提供的引用: - [^1]: Spring Boot启动类。 - [^2]: @MapperScan for MyBatis. - [^3]: WebMvcConfigurer for URL mapping. - [^4]: Elasticsearch mapping with analyzers. - [^5]: Elasticsearch configuration. 只有引用[2]可能与MyBatis相关。 用户的问题是关于循环引用在映射文件中,所以我应该专注于MyBatis或JPA。 在MyBatis中,处理循环引用通常涉及使用<resultMap>和<association>或<collection>,并可能使用lazy loading或自定义处理。 在JPA中,使用注解如@JsonManagedReference和@JsonBackReference来处理序列化。 由于用户提到“mapping 文件”,我假设是MyBatis。 步骤回答: 1. 解释问题:在ORM中,循环引用可能导致无限递归。 2. 在MyBatis中定义映射:使用<resultMap>定义实体,使用<association>或<collection>处理关系。 3. 处理循环引用:建议使用lazy loading或指定映射深度。 4. 示例代码:提供MyBatis映射文件的例子。 5. 在Spring Boot中集成。 生成相关问题: - 如何避免循环引用导致的无限递归? - MyBatis中如何处理一对多关系? -Spring Boot中使用MyBatis的最佳实践? 确保回答真实可靠:基于MyBatis文档。 添加引用:用户提供的引用可能不直接相关,但引用[2]提到@MapperScan,所以我可以引用它。 最终回答结构: - 介绍问题。 - 解释在MyBatis映射文件中定义关系。 - 处理循环引用的方法。 - 示例代码。 - 相关问题。</think>### Spring Boot 中映射文件定义循环引用的方法 在 Spring Boot 中,"映射文件" 通常指的是 ORM(对象关系映射)框架的配置文件,例如 MyBatis 的 XML 映射文件或 JPA 的实体注解。当实体之间存在循环引用(例如,实体 A 引用实体 B,实体 B 又引用实体 A)时,在序列化或数据加载时可能导致无限递归问题(如 StackOverflowError)。下面我将以 MyBatis 为例,逐步解释如何在映射文件中定义循环引用,并提供解决方案。MyBatis 是 Spring Boot 中常用的 ORM 框架,通过 XML 文件定义 SQL 和结果映射。 #### 步骤 1: 理解循环引用问题 在 ORM 中,循环引用常见于一对多或多对一关系中。例如: - 实体 `Order`(订单)包含一个 `Customer`(客户)引用。 - 实体 `Customer` 包含一个 `Order` 列表引用。 在 MyBatis 映射文件中,如果直接使用 `<association>` 或 `<collection>` 定义这种关系,可能导致无限递归加载。Spring Boot 集成 MyBatis 时,可以通过 `@MapperScan` 注解扫描映射接口,确保映射文件被正确加载[^2]。 #### 步骤 2: 在 MyBatis 映射文件中定义循环关系 MyBatis 使用 XML 文件定义结果映射。处理循环引用时,关键是通过 `<resultMap>` 的 `id` 和 `result` 属性关联实体,并使用 `lazyLoadingEnabled` 开启懒加载来避免立即加载所有关联数据。以下是定义循环引用的示例: 1. **定义实体类**: 假设有两个实体:`Order` 和 `Customer`。 ```java // Order.java public class Order { private Integer id; private String orderNumber; private Customer customer; // 引用 Customer 实体 // getters and setters } // Customer.java public class Customer { private Integer id; private String name; private List<Order> orders; // 引用 Order 列表 // getters and setters } ``` 2. **在 MyBatis 映射文件中定义结果映射**: 在 `OrderMapper.xml` 文件中,使用 `<resultMap>` 定义循环关系。关键点: - 使用 `<association>` 处理多对一关系(如 Order 引用 Customer)。 - 使用 `<collection>` 处理一对多关系(如 Customer 引用 Order 列表)。 - 通过 `column` 和 `property` 属性建立关联。 - 设置 `fetchType="lazy"` 启用懒加载,避免无限递归。 ```xml <!-- OrderMapper.xml --> <mapper namespace="com.example.mapper.OrderMapper"> <!-- 定义 Order 的结果映射,包含对 Customer 的引用 --> <resultMap id="OrderResultMap" type="Order"> <id property="id" column="order_id"/> <result property="orderNumber" column="order_number"/> <!-- 关联 Customer 实体,使用懒加载 --> <association property="customer" column="customer_id" javaType="Customer" fetchType="lazy" select="com.example.mapper.CustomerMapper.selectCustomerById"/> </resultMap> <!-- 查询 Order 的 SQL --> <select id="selectOrderById" resultMap="OrderResultMap"> SELECT * FROM orders WHERE id = #{id} </select> </mapper> <!-- CustomerMapper.xml --> <mapper namespace="com.example.mapper.CustomerMapper"> <!-- 定义 Customer 的结果映射,包含对 Order 列表的引用 --> <resultMap id="CustomerResultMap" type="Customer"> <id property="id" column="customer_id"/> <result property="name" column="name"/> <!-- 关联 Order 列表,使用懒加载 --> <collection property="orders" column="id" ofType="Order" fetchType="lazy" select="com.example.mapper.OrderMapper.selectOrdersByCustomerId"/> </resultMap> <!-- 查询 Customer 的 SQL --> <select id="selectCustomerById" resultMap="CustomerResultMap"> SELECT * FROM customers WHERE id = #{id} </select> <!-- 辅助查询:根据 Customer ID 获取 Order 列表 --> <select id="selectOrdersByCustomerId" resultMap="OrderResultMap"> SELECT * FROM orders WHERE customer_id = #{customerId} </select> </mapper> ``` 在上面的代码中: - `<association>` 用于 Order 到 Customer 的引用,通过 `select` 属性调用另一个映射文件的查询方法。 - `<collection>` 用于 Customer 到 Order 列表的引用,同样使用懒加载。 - `fetchType="lazy"` 确保关联数据只在需要时加载,防止立即递归。 #### 步骤 3: 处理循环引用的序列化问题 在 Spring Boot 中,即使映射文件定义了懒加载,序列化(如 JSON 输出)时仍可能触发无限递归。解决方法是: - **在实体类中添加 Jackson 注解**:使用 `@JsonManagedReference` 和 `@JsonBackReference` 标记循环引用端,避免序列化循环。 ```java // Order.java public class Order { // ... 其他属性 @JsonBackReference // 标记为“后端引用”,序列化时忽略 private Customer customer; // getters and setters } // Customer.java public class Customer { // ... 其他属性 @JsonManagedReference // 标记为“前端引用”,序列化时正常处理 private List<Order> orders; // getters and setters } ``` - **配置 MyBatis 懒加载**:在 `application.properties` 中启用全局懒加载。 ```properties mybatis.configuration.lazy-loading-enabled=true mybatis.configuration.aggressive-lazy-loading=false ``` #### 步骤 4: 在 Spring Boot 中集成 1. **添加依赖**:在 `pom.xml` 中包含 MyBatis 和数据库驱动。 ```xml <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> ``` 2. **配置 Mapper 扫描**:在启动类或配置类中使用 `@MapperScan` 扫描映射接口。 ```java @SpringBootApplication @MapperScan("com.example.mapper") // 扫描映射文件所在的包 public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` 这确保映射文件被 Spring Boot 加载[^2]。 3. **测试和验证**:编写 Controller 测试查询。如果出现序列化问题,检查 Jackson 注解和懒加载配置。 #### 注意事项 - **性能优化**:懒加载能避免无限递归,但可能增加数据库查询次数。考虑使用 DTO(Data Transfer Object)只返回必要字段。 - **其他 ORM 框架**:如果使用 JPA(如 Hibernate),可以通过 `@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)` 和 `@JsonIgnore` 注解处理循环引用,但映射文件方式与 MyBatis 不同。 - **避免常见错误**:确保映射文件的 `namespace` 和 `select` 属性路径正确,否则会抛出 `BindingException`。 通过以上步骤,您可以在 MyBatis 映射文件中安全地定义循环引用。如果问题涉及 Elasticsearch 映射(如引用[4][^4]和[5][^5]),处理方式不同,但循环引用问题在数据库 ORM 中更常见。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值