网上看了些教程,要么有错,要么过时,要么太过简陋。踩了几天坑,遇到的各种报错和莫名其妙的不起作用应该不下三十次。现在打算梳理一下,写成教程。
使用的 JDK 为 11.0.7,除了 xml 解析的库之外全部使用最新版本。
准备工作
配置环境、建库的事情非本文中心。就略过了。提一点必要的:
新建项目
依赖项
<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
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
username: root```
同目录下,创建 `hibernate.properties` (经测试,`yml` 格式不支持,不过支持 xml 格式,即写到 `hibernate.cfg.xml` 中)。
```propertieshibernate.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 配置文件,简单管理会话。内容如下:
```javapackage 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
,写入内容:
"-//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 之类的错误。
这个文件里也可以写数据库配置,不过我还是喜欢分开写 ——一个文件尽量只做一件事情。
如果还是想写,可以参考下面的。我试过了,没有问题:
"-//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
枚举。
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
是必须的,不填就运行报错。而表名、字段名都可以自动生成。
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
。添加一个函数:
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);
}
然后运行这个测试(点坐标绿色三角图标)。
然后会看见 Test passed 的提示。数据库中也能找到此刻创建的记录:
只是,创建的表未免太单薄了。结构也不够完美:
实际项目中这么做的话,和你一起写代码的人会很难受的。我们需要添加注释、限制字段的长度等更多额外的属性。
添加更多细节
下面具体来操作。所有注解用法,均参考 此文档,请收藏以备参阅。
让我们回到实体类。
添加表名
有时候我们的表名需要一个前缀,比如 module_table
这样的形式。方法如下:
public class User implements Serializable {
// ...```
添加之后出现红线不影响运行。如果看着难受,参考 [此文][9] 解决。
### 添加列名 {#rtoc-11}
方法如下:
```java @Column(name = "user_id")
private Long id;
自动添加更新时间和创建时间
下面说点真正有用的:如何让 created_at
和 updated_at
这两个字段自动更新?
直接增加两个方法:
protected void onCreate() {
createdAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
这里我用的是 LocalDateTime,因为这东西和时区无关,用起来顺手。最重要的是还线程安全。
添加列定义
如果我们手动进行列定义,就可以限制字段长、添加默认值和注释。当然,长度也可以通过 length
指定,通过 nullable
设置是否可 null
。
注意,一旦你自己接管 columnDefinition,那么数据类型也是你自己负责
例子如下:
private String username;
但是,这么做是糟糕的,因为这样相当于和 MySQL 耦合。要换数据库你可能还需要改代码。目前没有找到良好的注释方案。
而且存在另外的问题:生成的表是按照字母序的,而不是按照我们定义的顺序。
不过,我们可以另辟蹊径 —— 用 Flyway 创建数据表。可以构思一下流程 ——Hibernate 不直接生成表,而是生成 SQL Schema。程序运行之后,Flyway 读取 Schema 并更新表。
限于篇幅,以后再讲。挖个坑:
- Flyway 的集成
- 一对多的映射
附注
不需要添加 @EntityScan
注解到入口。
@EntityScan(basePackages = {"com.pluvet.occult.rest_server.entity"})
也不需要手写 user.hbm.xml
注解。