集成Hibernate
使用JdbcTemplate
的时候,我们用得最多的方法就是List<T> query(String sql, Object[] args, RowMapper rowMapper)
。这个RowMapper
的作用就是把ResultSet
的一行记录映射为Java Bean。
这种把关系数据库的表记录映射为Java对象的过程就是ORM:Object-Relational Mapping。ORM既可以把记录转换成Java对象,也可以把Java对象转换为行记录。
使用JdbcTemplate
配合RowMapper
可以看作是最原始的ORM。如果要实现更自动化的ORM,可以选择成熟的ORM框架,例如Hibernate。
我们来看看如何在Spring中集成Hibernate。
Hibernate作为ORM框架,它可以替代JdbcTemplate
,但Hibernate仍然需要JDBC驱动,所以,我们需要引入JDBC驱动、连接池,以及Hibernate本身。在Maven中,我们加入以下依赖项:
<!-- JDBC驱动,这里使用HSQLDB -->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.0</version>
</dependency>
<!-- JDBC连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.2</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.2.Final</version>
</dependency>
<!-- Spring Context和Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
在AppConfig中,我们仍然需要创建DataSource、引入JDBC配置文件,以及启用声明式事务:
@Configuration
@ComponentScan
@EnableTransactionManagement
@PropertySource("jdbc.properties")
public class AppConfig {
@Bean
DataSource createDataSource() {
...
}
}
为了启用Hibernate,我们需要创建一个LocalSessionFactoryBean
:
public class AppConfig {
@Bean
LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource) {
var props = new Properties();
props.setProperty("hibernate.hbm2ddl.auto", "update"); // 生产环境不要使用
props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
props.setProperty("hibernate.show_sql", "true");
var sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
// 扫描指定的package获取所有entity class:
sessionFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
sessionFactoryBean.setHibernateProperties(props);
return sessionFactoryBean;
}
}
注意我们在定制Bean中讲到过FactoryBean
,LocalSessionFactoryBean
是一个FactoryBean
,它会再自动创建一个SessionFactory
,在Hibernate中,Session
是封装了一个JDBC Connection
的实例,而SessionFactory
是封装了JDBC DataSource
的实例,即SessionFactory
持有连接池,每次需要操作数据库的时候,SessionFactory
创建一个新的Session
,相当于从连接池获取到一个新的Connection
。SessionFactory
就是Hibernate提供的最核心的一个对象,但LocalSessionFactoryBean
是Spring提供的为了让我们方便创建SessionFactory
的类。
注意到上面创建LocalSessionFactoryBean
的代码,首先用Properties
持有Hibernate初始化SessionFactory
时用到的所有设置,常用的设置请参考Hibernate文档,这里我们只定义了3个设置:
hibernate.hbm2ddl.auto=update
:表示自动创建数据库的表结构,注意不要在生产环境中启用;hibernate.dialect=org.hibernate.dialect.HSQLDialect
:指示Hibernate使用的数据库是HSQLDB。Hibernate使用一种HQL的查询语句,它和SQL类似,但真正在“翻译”成SQL时,会根据设定的数据库“方言”来生成针对数据库优化的SQL;hibernate.show_sql=true
:让Hibernate打印执行的SQL,这对于调试非常有用,我们可以方便地看到Hibernate生成的SQL语句是否符合我们的预期。
除了设置DataSource
和Properties
之外,注意到setPackagesToScan()
我们传入了一个package
名称,它指示Hibernate扫描这个包下面的所有Java类,自动找出能映射为数据库表记录的JavaBean。后面我们会仔细讨论如何编写符合Hibernate要求的JavaBean。
紧接着,我们还需要创建HibernateTemplate
以及HibernateTransactionManager
:
public class AppConfig {
@Bean
HibernateTemplate createHibernateTemplate(@Autowired SessionFactory sessionFactory) {
return new HibernateTemplate(sessionFactory);
}
@Bean
PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory) {
return new HibernateTransactionManager(sessionFactory);
}
}
这两个Bean的创建都十分简单。HibernateTransactionManager
是配合Hibernate使用声明式事务所必须的,而HibernateTemplate
则是Spring为了便于我们使用Hibernate提供的工具类,不是非用不可,但推荐使用以简化代码。
到此为止,所有的配置都定义完毕,我们来看看如何将数据库表结构映射为Java对象。
考察如下的数据库表:
CREATE TABLE user
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
password VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
createdAt BIGINT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
);
其中,id
是自增主键,email
、password
、name
是VARCHAR
类型,email
带唯一索引以确保唯一性,createdAt
存储整型类型的时间戳。用JavaBean表示如下:
public class User {
private Long id;
private String email;
private String password;
private String name;
private Long createdAt;
// getters and setters
...
}
这种映射关系十分易懂,但我们需要添加一些注解来告诉Hibernate如何把User
类映射到表记录:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
public Long getId() { ... }
@Column(nullable = false, unique = true, length = 100)
public String getEmail() { ... }
@Column(nullable = false, length = 100)
public String getPassword() { ... }
@Column(nullable = false, length = 100)
public String getName() { ... }
@Column(nullable = false, updatable = false)
public Long getCreatedAt() { ... }
}
如果一个JavaBean被用于映射,我们就标记一个@Entity
。默认情况下,映射的表名是user
,如果实际的表名不同,例如实际表名是users
,可以追加一个@Table(name="users")
表示:
@Entity
@Table(name="users)
public class User {
...
}
每个属性到数据库列的映射用@Column()
标识,nullable
指示列是否允许为NULL
,updatable
指示该列是否允许被用在UPDATE
语句,length
指示String
类型的列的长度(如果没有指定,默认是255
)。
对于主键,还需要用@Id
标识,自增主键再追加一个@GeneratedValue
,以便Hibernate能读取到自增主键的值。
细心的童鞋可能还注意到,主键id
定义的类型不是long
,而是Long
。这是因为Hibernate如果检测到主键为null
,就不会在INSERT
语句中指定主键的值,而是返回由数据库生成的自增值,否则,Hibernate认为我们的程序指定了主键的值,会在INSERT
语句中直接列出。long
型字段总是具有默认值0
,因此,每次插入的主键值总是0,导致除第一次外后续插入都将失败。
createdAt
虽然是整型,但我们并没有使用long
,而是Long
,这是因为使用基本类型会导致某种查询会添加意外的条件,后面我们会详细讨论,这里只需牢记,作为映射使用的JavaBean,所有属性都使用包装类型而不是基本类型。
使用Hibernate时,不要使用基本类型的属性,总是使用包装类型,如Long或Integer。
类似的,我们再定义一个Book
类:
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
public Long getId() { ... }
@Column(nullable = false, length = 100)
public String getTitle() { ... }
@Column(nullable = false, updatable = false)
public Long getCreatedAt() { ... }
}
如果仔细观察User
和Book
,会发现它们定义的id
、createdAt
属性是一样的,这在数据库表结构的设计中很常见:对于每个表,通常我们会统一使用一种主键生成机制,并添加createdAt
表示创建时间,updatedAt
表示修改时间等通用字段。
不必在User
和Book
中重复定义这些通用字段,我们可以把它们提到一个抽象类中:
@MappedSuperclass
public abstract class AbstractEntity {
private Long id;
private Long createdAt;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
public Long getId() { ... }
@Column(nullable = false, updatable = false)
public Long getCreatedAt() { ... }
@Transient
public ZonedDateTime getCreatedDateTime() {
return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
}
@PrePersist
public void preInsert() {
setCreatedAt(System.currentTimeMillis());
}
}
对于AbstractEntity
来说,我们要标注一个@MappedSuperclass
表示它用于继承。此外,注意到我们定义了一个@Transient
方法,它返回一个“虚拟”的属性。因为getCreatedDateTime()
是计算得出的属性,而不是从数据库表读出的值,因此必须要标注@Transient
,否则Hibernate会尝试从数据库读取名为createdDateTime
这个不存在的字段从而出错。
再注意到@PrePersist
标识的方法,它表示在我们将一个JavaBean持久化到数据库之前(即执行INSERT语句),Hibernate会先执行该方法,这样我们就可以自动设置好createdAt
属性。
有了AbstractEntity
,我们就可以大幅简化User
和Book
:
@Entity
public class User extends AbstractEntity {
@Column(nullable = false, unique = true, length = 100)
public String getEmail() { ... }
@Column(nullable = false, length = 100)
public String getPassword() { ... }
@Column(nullable = false, length = 100)
public String getName() { ... }
}
注意到使用的所有注解均来自javax.persistence
,它是JPA规范的一部分。这里我们只介绍使用注解的方式配置Hibernate映射关系,不再介绍传统的比较繁琐的XML配置。通过Spring集成Hibernate时,也不再需要hibernate.cfg.xml
配置文件,用一句话总结:
使用Spring集成Hibernate,配合JPA注解,无需任何额外的XML配置。
类似User
、Book
这样的用于ORM的Java Bean,我们通常称之为Entity Bean。
最后,我们来看看如果对user
表进行增删改查。因为使用了Hibernate,因此,我们要做的,实际上是对User
这个JavaBean进行“增删改查”。我们编写一个UserService
,注入HibernateTemplate
以便简化代码:
@Component
@Transactional
public class UserService {
@Autowired
HibernateTemplate hibernateTemplate;
}
Insert操作
要持久化一个User
实例,我们只需调用save()
方法。以register()
方法为例,代码如下:
public User register(String email, String password, String name) {
// 创建一个User对象:
User user = new User();
// 设置好各个属性:
user.setEmail(email);
user.setPassword(password);
user.setName(name);
// 不要设置id,因为使用了自增主键
// 保存到数据库:
hibernateTemplate.save(user);
// 现在已经自动获得了id:
System.out.println(user.getId());
return user;
}
Delete操作
删除一个User
相当于从表中删除对应的记录。注意Hibernate总是用id
来删除记录,因此,要正确设置User
的id
属性才能正常删除记录:
public boolean deleteUser(Long id) {
User user = hibernateTemplate.get(User.class, id);
if (user != null) {
hibernateTemplate.delete(user);
return true;
}
return false;
}
通过主键删除记录时,一个常见的用法是先根据主键加载该记录,再删除。load()
和get()
都可以根据主键加载记录,它们的区别在于,当记录不存在时,get()
返回null
,而load()
抛出异常。
Update操作
更新记录相当于先更新User
的指定属性,然后调用update()
方法:
public void updateUser(Long id, String name) {
User user = hibernateTemplate.load(User.class, id);
user.setName(name);
hibernateTemplate.update(user);
}
前面我们在定义User
时,对有的属性标注了@Column(updatable=false)
。Hibernate在更新记录时,它只会把@Column(updatable=true)
的属性加入到UPDATE
语句中,这样可以提供一层额外的安全性,即如果不小心修改了User
的email
、createdAt
等属性,执行update()
时并不会更新对应的数据库列。但也必须牢记:这个功能是Hibernate提供的,如果绕过Hibernate直接通过JDBC执行UPDATE
语句仍然可以更新数据库的任意列的值。
最后,我们编写的大部分方法都是各种各样的查询。根据id
查询我们可以直接调用load()
或get()
,如果要使用条件查询,有3种方法。
假设我们想执行以下查询:
SELECT * FROM user WHERE email = ? AND password = ?
我们来看看可以使用什么查询。
使用Example查询
第一种方法是使用findByExample()
,给出一个User
实例,Hibernate把该实例所有非null
的属性拼成WHERE
条件:
public User login(String email, String password) {
User example = new User();
example.setEmail(email);
example.setPassword(password);
List<User> list = hibernateTemplate.findByExample(example);
return list.isEmpty() ? null : list.get(0);
}
因为example
实例只有email
和password
两个属性为非null
,所以最终生成的WHERE
语句就是WHERE email = ? AND password = ?
。
如果我们把User
的createdAt
的类型从Long
改为long
,findByExample()
的查询将出问题,原因在于example
实例的long
类型字段有了默认值0,导致Hibernate最终生成的WHERE
语句意外变成了WHERE email = ? AND password = ? AND createdAt = 0
。显然,额外的查询条件将导致错误的查询结果。
使用findByExample()时,注意基本类型字段总是会加入到WHERE条件!
使用Criteria查询
第二种查询方法是使用Criteria查询,可以实现如下:
public User login(String email, String password) {
DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
criteria.add(Restrictions.eq("email", email))
.add(Restrictions.eq("password", password));
List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
return list.isEmpty() ? null : list.get(0);
}
DetachedCriteria
使用链式语句来添加多个AND
条件。和findByExample()
相比,findByCriteria()
可以组装出更灵活的WHERE
条件,例如:
SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?
上述查询没法用findByExample()
实现,但用Criteria查询可以实现如下:
DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
criteria.add(
Restrictions.and(
Restrictions.or(
Restrictions.eq("email", email),
Restrictions.eq("name", email)
),
Restrictions.eq("password", password)
)
);
只要组织好Restrictions
的嵌套关系,Criteria查询可以实现任意复杂的查询。
使用HQL查询
最后一种常用的查询是直接编写Hibernate内置的HQL查询:
List<User> list = (List<User>) hibernateTemplate.find("FROM User WHERE email=? AND password=?", email, password);
和SQL相比,HQL使用类名和属性名,由Hibernate自动转换为实际的表名和列名。详细的HQL语法可以参考Hibernate文档。
除了可以直接传入HQL字符串外,Hibernate还可以使用一种NamedQuery
,它给查询起个名字,然后保存在注解中。使用NamedQuery
时,我们要先在User
类标注:
@NamedQueries(
@NamedQuery(
// 查询名称:
name = "login",
// 查询语句:
query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
)
)
@Entity
public class User extends AbstractEntity {
...
}
注意到引入的NamedQuery是javax.persistence.NamedQuery
,它和直接传入HQL有点不同的是,占位符使用?0
、?1
,并且索引是从0
开始的(真乱)。
使用NamedQuery
只需要引入查询名和参数:
public User login(String email, String password) {
List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login", email, password);
return list.isEmpty() ? null : list.get(0);
}
直接写HQL和使用NamedQuery
各有优劣。前者可以在代码中直观地看到查询语句,后者可以在User
类统一管理所有相关查询。
使用Hibernate原生接口
如果要使用Hibernate原生接口,但不知道怎么写,可以参考HibernateTemplate
的源码。使用Hibernate的原生接口实际上总是从SessionFactory
出发,它通常用全局变量存储,在HibernateTemplate
中以成员变量被注入。有了SessionFactory
,使用Hibernate用法如下:
void operation() {
Session session = null;
boolean isNew = false;
// 获取当前Session或者打开新的Session:
try {
session = this.sessionFactory.getCurrentSession();
} catch (HibernateException e) {
session = this.sessionFactory.openSession();
isNew = true;
}
// 操作Session:
try {
User user = session.load(User.class, 123L);
}
finally {
// 关闭新打开的Session:
if (isNew) {
session.close();
}
}
}
练习
从下载练习:集成Hibernate (推荐使用IDE练习插件快速下载)
小结
在Spring中集成Hibernate需要配置的Bean如下:
- DataSource;
- LocalSessionFactory;
- HibernateTransactionManager;
- HibernateTemplate(推荐)。
推荐使用Annotation配置所有的Entity Bean。
读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论: