Site Overlay

SpringBoot + JPA(Hibernate)自动建表实战

网上看了些教程,要么有错,要么过时,要么太过简陋。踩了几天坑,遇到的各种报错和莫名其妙的不起作用应该不下三十次。现在打算梳理一下,写成教程。

使用的 JDK 为 11.0.7,除了 xml 解析的库之外全部使用最新版本。

准备工作

配置环境、建库的事情非本文中心。就略过了。提一点必要的:

新建项目

typora\20200705165001_a3df3ecb83db04bec526d17d476161d1.png

typora\20200705165418_96928421d955952d7a838efbfbf83642.png

typora\20200705165547_0997b82d98963cb73614544fbb592075.png

typora\20200705165612_9a2e058a878a64a33c747e2c76d4ff17.png

依赖项

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.3.7.Final</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-impl -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-core -->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0.1</version>
        </dependency>

    </dependencies>

配置文件

我是用的 MySQL 版本为 5.7,请酌情修改下述配置。

首先是:src\main\resources\application.yml

server:
  port: 8081
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: root
    url: jdbc:mysql://localhost:3306/devtest?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8
    username: root

同目录下,创建 hibernate.properties (经测试,yml 格式不支持,不过支持 xml 格式,即写到 hibernate.cfg.xml 中)。

hibernate.connection.driver_class = com.mysql.cj.jdbc.Driver
hibernate.connection.url = jdbc:mysql://localhost:3306/devtest?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8
hibernate.connection.username = root
hibernate.connection.password = root
hibernate.dialect = org.hibernate.dialect.MySQL57Dialect
hibernate.hbm2ddl.auto = update

HibernateUtil 工具类

创建 com.pluvet.auto_table_demo.util.HibernateUtil。其负责读取 hibernate 配置文件,简单管理会话。内容如下:

package com.pluvet.auto_table_demo.util;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class HibernateUtil {

    private static SessionFactory factory = null;

    static {
        var serviceRegistry = new StandardServiceRegistryBuilder().configure().build();
        factory = new MetadataSources(serviceRegistry).buildMetadata().buildSessionFactory();
    }

    public static Session getSession() {
        Session ses = null;
        if (factory != null)
            ses = factory.openSession();
        return ses;
    }

    public static void closeSession(Session ses) {
        if (ses != null)
            ses.close();
    }

    public static void closeFactory() {
        if (factory != null)
            factory.close();
    }
}

映射文件

hibernate.properties 的同目录下,创建 hibernate.cfg.xml,写入内容:

<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <mapping class="com.pluvet.auto_table_demo.entity.User"/>
    </session-factory>
</hibernate-configuration>

没错,这里就写我们的类映射了,说通俗点就是类和数据表的对应关系。这个类我们等一下再创建。

注意:以后每创建一个实体类,都要来这添加对应的映射。不然的话会出现 Entity not found 之类的错误。

这个文件里也可以写数据库配置,不过我还是喜欢分开写——一个文件尽量只做一件事情

如果还是想写,可以参考下面的。我试过了,没有问题:

<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>

    <session-factory>

        <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/devtest?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL57Dialect</property>
        <property name="hibernate.hbm2ddl.auto">update</property>

        <property name="hibernate.archive.autodetection">class</property>

        <mapping class="com.pluvet.occult.rest_server.entity.User"/>

    </session-factory>
</hibernate-configuration>

实体类

实体类说白了就是数据表结构以 Java 类表示。我们想要创建一个用户类,并且用户包含一个枚举字段,表示用户的状态。

建立包 com.pluvet.auto_table_demo.model,创建 UserStatusEnum 枚举。

package com.pluvet.auto_table_demo.model;

public enum UserStatusEnum {
    Normal(0, "正常"),
    Unverified(1, "未激活"),
    Blocked(2, "已封禁"),
    Closed(99, "已注销");

    UserStatusEnum(int status, String description){
        this.status = status;
        this.description = description;
    }

    private final int status;

    private final String description;
}

建立包 com.pluvet.auto_table_demo.entity,创建 User 类。

注意:strategy = GenerationType.IDENTITY 是必须的,不填就运行报错。而表名、字段名都可以自动生成。

package com.pluvet.auto_table_demo.entity;

import com.pluvet.auto_table_demo.model.UserStatusEnum;
import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 *  用户
 * </p>
 *
 * @author Zhang Zijing (Pluveto) <i@pluvet.com>
 * @since 2020-07-04
 */
@Data
@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 显示名
     */
    private String screenName;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 状态 0 表示正常,1 表示未激活,2 表示禁用,99 表示删除
     */
    private UserStatusEnum status;
    /**
     * 创建时间
     */
    private LocalDateTime createdAt;

    /**
     * 更新时间
     */
    private LocalDateTime updatedAt;

    /**
     * 最后登录时间
     */
    private LocalDateTime activeAt;

}

运行

我们写一个测试来运行。

编辑测试类 src\test\java\com\pluvet\auto_table_demo\AutoTableDemoApplicationTests.java。添加一个函数:

    @Test
    void HibernateTest() {
        var user = new User();
        user.setUsername("pluvet");
        user.setEmail("test@mail.com");
        user.setPassword("pass11111111");
        user.setScreenName("Pluvet");
        user.setStatus(UserStatusEnum.Normal);

        var session = HibernateUtil.getSession();
        var transaction = session.beginTransaction();
        session.save(user);
        transaction.commit();
        HibernateUtil.closeSession(session);
    }

然后运行这个测试(点坐标绿色三角图标)。

typora\20200705172907_41e649556198e9f4ef13209b003b9b8c.png

然后会看见 Test passed 的提示。数据库中也能找到此刻创建的记录:

typora\20200705173151_bff1ad2ae9f631196a9d31322aa03647.png

只是,创建的表未免太单薄了。结构也不够完美:

typora\20200705173241_78389f8fd9360ef37b01f5152e7ad6e4.png

实际项目中这么做的话,和你一起写代码的人会很难受的。我们需要添加注释、限制字段的长度等更多额外的属性

添加更多细节

下面具体来操作。所有注解用法,均参考此文档,请收藏以备参阅。

让我们回到实体类。

添加表名

有时候我们的表名需要一个前缀,比如 module_table 这样的形式。方法如下:

@Table(name = "auth_user")
public class User implements Serializable {
    // ...

添加之后出现红线不影响运行。如果看着难受,参考此文解决。

添加列名

方法如下:

    @Column(name = "user_id")
    private Long id;

自动添加更新时间和创建时间

下面说点真正有用的:如何让 created_atupdated_at 这两个字段自动更新?

直接增加两个方法:

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

这里我用的是 LocalDateTime,因为这东西和时区无关,用起来顺手。最重要的是还线程安全。

添加列定义

如果我们手动进行列定义,就可以限制字段长、添加默认值和注释。当然,长度也可以通过 length 指定,通过 nullable 设置是否可 null

注意,一旦你自己接管 columnDefinition,那么数据类型也是你自己负责

例子如下:

    @Column(nullable = false,columnDefinition = "varchar(20) COMMENT '用户名'")
    private String username;

但是,这么做是糟糕的,因为这样相当于和 MySQL 耦合。要换数据库你可能还需要改代码。目前没有找到良好的注释方案。

而且存在另外的问题:生成的表是按照字母序的,而不是按照我们定义的顺序。

不过,我们可以另辟蹊径——用 Flyway 创建数据表。可以构思一下流程——Hibernate 不直接生成表,而是生成 SQL Schema。程序运行之后,Flyway 读取 Schema 并更新表。

限于篇幅,以后再讲。挖个坑:

  1. Flyway 的集成
  2. 一对多的映射

附注

不需要添加 @EntityScan 注解到入口。

@EntityScan(basePackages = {"com.pluvet.occult.rest_server.entity"})

也不需要手写 user.hbm.xml 注解。

发表评论

电子邮件地址不会被公开。 必填项已用*标注