廖雪峰

资深软件开发工程师,业余马拉松选手。

使用JdbcTemplate的时候,我们用得最多的方法就是List query(String, RowMapper, Object...)。这个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中,我们加入以下依赖项:

org.springframework:spring-context:6.0.0

org.springframework:spring-orm:6.0.0

jakarta.annotation:jakarta.annotation-api:2.1.1

jakarta.persistence:jakarta.persistence-api:3.1.0

org.hibernate:hibernate-core:6.1.4.Final

com.zaxxer:HikariCP:5.0.1

org.hsqldb:hsqldb:2.7.1

在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。

紧接着,我们还需要创建HibernateTransactionManager:

public class AppConfig {

@Bean

PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory) {

return new HibernateTransactionManager(sessionFactory);

}

}

HibernateTransactionManager是配合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,这是因为使用基本类型会导致findByExample查询会添加意外的条件,这里只需牢记,作为映射使用的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() { ... }

}

注意到使用的所有注解均来自jakarta.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,注入SessionFactory:

@Component

@Transactional

public class UserService {

@Autowired

SessionFactory sessionFactory;

}

Insert操作

要持久化一个User实例,我们只需调用persist()方法。以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,因为使用了自增主键

// 保存到数据库:

sessionFactory.getCurrentSession().persist(user);

// 现在已经自动获得了id:

System.out.println(user.getId());

return user;

}

Delete操作

删除一个User相当于从表中删除对应的记录。注意Hibernate总是用id来删除记录,因此,要正确设置User的id属性才能正常删除记录:

public boolean deleteUser(Long id) {

User user = sessionFactory.getCurrentSession().byId(User.class).load(id);

if (user != null) {

sessionFactory.getCurrentSession().remove(user);

return true;

}

return false;

}

通过主键删除记录时,一个常见的用法是先根据主键加载该记录,再删除。注意到当记录不存在时,load()返回null。

Update操作

更新记录相当于先更新User的指定属性,然后调用merge()方法:

public void updateUser(Long id, String name) {

User user = sessionFactory.getCurrentSession().byId(User.class).load(id);

user.setName(name);

sessionFactory.getCurrentSession().merge(user);

}

前面我们在定义User时,对有的属性标注了@Column(updatable=false)。Hibernate在更新记录时,它只会把@Column(updatable=true)的属性加入到UPDATE语句中,这样可以提供一层额外的安全性,即如果不小心修改了User的email、createdAt等属性,执行update()时并不会更新对应的数据库列。但也必须牢记:这个功能是Hibernate提供的,如果绕过Hibernate直接通过JDBC执行UPDATE语句仍然可以更新数据库的任意列的值。

最后,我们编写的大部分方法都是各种各样的查询。根据id查询我们可以直接调用load(),如果要使用条件查询,例如,假设我们想执行以下查询:

SELECT * FROM user WHERE email = ? AND password = ?

我们来看看可以使用什么查询。

使用HQL查询

一种常用的查询是直接编写Hibernate内置的HQL查询:

List list = sessionFactory.getCurrentSession()

.createQuery("from User u where u.email = ?1 and u.password = ?2", User.class)

.setParameter(1, email).setParameter(2, password)

.list();

和SQL相比,HQL使用类名和属性名,由Hibernate自动转换为实际的表名和列名。详细的HQL语法可以参考Hibernate文档。

除了可以直接传入HQL字符串外,Hibernate还可以使用一种NamedQuery,它给查询起个名字,然后保存在注解中。使用NamedQuery时,我们要先在User类标注:

@NamedQueries(

@NamedQuery(

// 查询名称:

name = "login",

// 查询语句:

query = "SELECT u FROM User u WHERE u.email = :e AND u.password = :pwd"

)

)

@Entity

public class User extends AbstractEntity {

...

}

注意到引入的NamedQuery是jakarta.persistence.NamedQuery,它和直接传入HQL有点不同的是,占位符使用:e和:pwd。

使用NamedQuery只需要引入查询名和参数:

public User login(String email, String password) {

List list = sessionFactory.getCurrentSession()

.createNamedQuery("login", User.class) // 创建NamedQuery

.setParameter("e", email) // 绑定e参数

.setParameter("pwd", password) // 绑定pwd参数

.list();

return list.isEmpty() ? null : list.get(0);

}

直接写HQL和使用NamedQuery各有优劣。前者可以在代码中直观地看到查询语句,后者可以在User类统一管理所有相关查询。

练习

集成Hibernate操作数据库。

下载练习

小结

在Spring中集成Hibernate需要配置的Bean如下:

DataSource;

LocalSessionFactory;

HibernateTransactionManager。

推荐使用Annotation配置所有的Entity Bean。


wps是什么软件怎么用
ps怎么输出(保存)动画的方法