这种把关系数据库的表记录映射为Java对象的过程就是ORM:Object-Relational Mapping。ORM既可以把记录转换成Java对象,也可以把Java对象转换为行记录。

    使用JdbcTemplate配合RowMapper可以看作是最原始的ORM。如果要实现更自动化的ORM,可以选择成熟的ORM框架,例如。

    我们来看看如何在Spring中集成Hibernate。

    Hibernate作为ORM框架,它可以替代JdbcTemplate,但Hibernate仍然需要JDBC驱动,所以,我们需要引入JDBC驱动、连接池,以及Hibernate本身。在Maven中,我们加入以下依赖项:

    在AppConfig中,我们仍然需要创建DataSource、引入JDBC配置文件,以及启用声明式事务:

    1. @Configuration
    2. @ComponentScan
    3. @EnableTransactionManagement
    4. @PropertySource("jdbc.properties")
    5. public class AppConfig {
    6. @Bean
    7. DataSource createDataSource() {
    8. ...
    9. }
    10. }

    为了启用Hibernate,我们需要创建一个LocalSessionFactoryBean

    1. public class AppConfig {
    2. @Bean
    3. LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource) {
    4. var props = new Properties();
    5. props.setProperty("hibernate.hbm2ddl.auto", "update"); // 生产环境不要使用
    6. props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
    7. props.setProperty("hibernate.show_sql", "true");
    8. var sessionFactoryBean = new LocalSessionFactoryBean();
    9. sessionFactoryBean.setDataSource(dataSource);
    10. // 扫描指定的package获取所有entity class:
    11. sessionFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
    12. sessionFactoryBean.setHibernateProperties(props);
    13. return sessionFactoryBean;
    14. }
    15. }

    注意我们在定制Bean中讲到过FactoryBeanLocalSessionFactoryBean是一个FactoryBean,它会再自动创建一个SessionFactory,在Hibernate中,Session是封装了一个JDBC Connection的实例,而SessionFactory是封装了JDBC DataSource的实例,即SessionFactory持有连接池,每次需要操作数据库的时候,SessionFactory创建一个新的Session,相当于从连接池获取到一个新的ConnectionSessionFactory就是Hibernate提供的最核心的一个对象,但LocalSessionFactoryBean是Spring提供的为了让我们方便创建SessionFactory的类。

    注意到上面创建LocalSessionFactoryBean的代码,首先用Properties持有Hibernate初始化SessionFactory时用到的所有设置,常用的设置请参考,这里我们只定义了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语句是否符合我们的预期。

    除了设置DataSourceProperties之外,注意到setPackagesToScan()我们传入了一个package名称,它指示Hibernate扫描这个包下面的所有Java类,自动找出能映射为数据库表记录的JavaBean。后面我们会仔细讨论如何编写符合Hibernate要求的JavaBean。

    紧接着,我们还需要创建HibernateTemplate以及HibernateTransactionManager

    1. public class AppConfig {
    2. @Bean
    3. HibernateTemplate createHibernateTemplate(@Autowired SessionFactory sessionFactory) {
    4. return new HibernateTemplate(sessionFactory);
    5. }
    6. @Bean
    7. PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory) {
    8. return new HibernateTransactionManager(sessionFactory);
    9. }
    10. }

    这两个Bean的创建都十分简单。HibernateTransactionManager是配合Hibernate使用声明式事务所必须的,而HibernateTemplate则是Spring为了便于我们使用Hibernate提供的工具类,不是非用不可,但推荐使用以简化代码。

    到此为止,所有的配置都定义完毕,我们来看看如何将数据库表结构映射为Java对象。

    考察如下的数据库表:

    1. CREATE TABLE user
    2. id BIGINT NOT NULL AUTO_INCREMENT,
    3. email VARCHAR(100) NOT NULL,
    4. password VARCHAR(100) NOT NULL,
    5. name VARCHAR(100) NOT NULL,
    6. createdAt BIGINT NOT NULL,
    7. PRIMARY KEY (`id`),
    8. UNIQUE KEY `email` (`email`)
    9. );

    其中,id是自增主键,emailpasswordnameVARCHAR类型,email带唯一索引以确保唯一性,createdAt存储整型类型的时间戳。用JavaBean表示如下:

    1. public class User {
    2. private Long id;
    3. private String email;
    4. private String password;
    5. private String name;
    6. private Long createdAt;
    7. // getters and setters
    8. ...
    9. }

    这种映射关系十分易懂,但我们需要添加一些注解来告诉Hibernate如何把User类映射到表记录:

    1. @Entity
    2. public class User {
    3. @Id
    4. @GeneratedValue(strategy = GenerationType.IDENTITY)
    5. @Column(nullable = false, updatable = false)
    6. @Column(nullable = false, unique = true, length = 100)
    7. public String getEmail() { ... }
    8. @Column(nullable = false, length = 100)
    9. public String getPassword() { ... }
    10. @Column(nullable = false, length = 100)
    11. public String getName() { ... }
    12. @Column(nullable = false, updatable = false)
    13. public Long getCreatedAt() { ... }
    14. }

    如果一个JavaBean被用于映射,我们就标记一个@Entity。默认情况下,映射的表名是user,如果实际的表名不同,例如实际表名是users,可以追加一个@Table(name="users")表示:

    1. @Table(name="users)
    2. public class User {
    3. ...
    4. }

    每个属性到数据库列的映射用@Column()标识,nullable指示列是否允许为NULLupdatable指示该列是否允许被用在UPDATE语句,length指示String类型的列的长度(如果没有指定,默认是255)。

    对于主键,还需要用@Id标识,自增主键再追加一个@GeneratedValue,以便Hibernate能读取到自增主键的值。

    createdAt虽然是整型,但我们并没有使用long,而是Long,这是因为使用基本类型会导致某种查询会添加意外的条件,后面我们会详细讨论,这里只需牢记,作为映射使用的JavaBean,所有属性都使用包装类型而不是基本类型。

    使用Hibernate时,不要使用基本类型的属性,总是使用包装类型,如Long或Integer。

    类似的,我们再定义一个Book类:

    如果仔细观察UserBook,会发现它们定义的idcreatedAt属性是一样的,这在数据库表结构的设计中很常见:对于每个表,通常我们会统一使用一种主键生成机制,并添加createdAt表示创建时间,updatedAt表示修改时间等通用字段。

    不必在UserBook中重复定义这些通用字段,我们可以把它们提到一个抽象类中:

    1. @MappedSuperclass
    2. public abstract class AbstractEntity {
    3. private Long id;
    4. private Long createdAt;
    5. @Id
    6. @GeneratedValue(strategy = GenerationType.IDENTITY)
    7. @Column(nullable = false, updatable = false)
    8. public Long getId() { ... }
    9. @Column(nullable = false, updatable = false)
    10. public Long getCreatedAt() { ... }
    11. @Transient
    12. public ZonedDateTime getCreatedDateTime() {
    13. return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
    14. }
    15. @PrePersist
    16. public void preInsert() {
    17. setCreatedAt(System.currentTimeMillis());
    18. }
    19. }

    对于AbstractEntity来说,我们要标注一个@MappedSuperclass表示它用于继承。此外,注意到我们定义了一个@Transient方法,它返回一个“虚拟”的属性。因为getCreatedDateTime()是计算得出的属性,而不是从数据库表读出的值,因此必须要标注@Transient,否则Hibernate会尝试从数据库读取名为createdDateTime这个不存在的字段从而出错。

    再注意到@PrePersist标识的方法,它表示在我们将一个JavaBean持久化到数据库之前(即执行INSERT语句),Hibernate会先执行该方法,这样我们就可以自动设置好createdAt属性。

    有了AbstractEntity,我们就可以大幅简化UserBook

    1. @Entity
    2. public class User extends AbstractEntity {
    3. @Column(nullable = false, unique = true, length = 100)
    4. public String getEmail() { ... }
    5. @Column(nullable = false, length = 100)
    6. public String getPassword() { ... }
    7. @Column(nullable = false, length = 100)
    8. public String getName() { ... }
    9. }

    注意到使用的所有注解均来自javax.persistence,它是JPA规范的一部分。这里我们只介绍使用注解的方式配置Hibernate映射关系,不再介绍传统的比较繁琐的XML配置。通过Spring集成Hibernate时,也不再需要hibernate.cfg.xml配置文件,用一句话总结:

    使用Spring集成Hibernate,配合JPA注解,无需任何额外的XML配置。

    类似UserBook这样的用于ORM的Java Bean,我们通常称之为Entity Bean。

    最后,我们来看看如果对user表进行增删改查。因为使用了Hibernate,因此,我们要做的,实际上是对User这个JavaBean进行“增删改查”。我们编写一个UserService,注入HibernateTemplate以便简化代码:

    1. @Component
    2. @Transactional
    3. public class UserService {
    4. @Autowired
    5. HibernateTemplate hibernateTemplate;
    6. }

    要持久化一个User实例,我们只需调用save()方法。以register()方法为例,代码如下:

    1. public User register(String email, String password, String name) {
    2. // 创建一个User对象:
    3. User user = new User();
    4. // 设置好各个属性:
    5. user.setEmail(email);
    6. user.setPassword(password);
    7. user.setName(name);
    8. // 不要设置id,因为使用了自增主键
    9. // 保存到数据库:
    10. hibernateTemplate.save(user);
    11. // 现在已经自动获得了id:
    12. System.out.println(user.getId());
    13. return user;

    Delete操作

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

    1. public boolean deleteUser(Long id) {
    2. User user = hibernateTemplate.get(User.class, id);
    3. if (user != null) {
    4. hibernateTemplate.delete(user);
    5. return true;
    6. }
    7. }

    通过主键删除记录时,一个常见的用法是先根据主键加载该记录,再删除。load()get()都可以根据主键加载记录,它们的区别在于,当记录不存在时,get()返回null,而load()抛出异常。

    Update操作

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

    1. public void updateUser(Long id, String name) {
    2. User user = hibernateTemplate.load(User.class, id);
    3. user.setName(name);
    4. hibernateTemplate.update(user);
    5. }

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

    最后,我们编写的大部分方法都是各种各样的查询。根据id查询我们可以直接调用load()get(),如果要使用条件查询,有3种方法。

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

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

    第一种方法是使用findByExample(),给出一个User实例,Hibernate把该实例所有非null的属性拼成WHERE条件:

    因为example实例只有emailpassword两个属性为非null,所以最终生成的WHERE语句就是WHERE email = ? AND password = ?

    如果我们把UsercreatedAt的类型从Long改为longfindByExample()的查询将出问题,原因在于example实例的long类型字段有了默认值0,导致Hibernate最终生成的WHERE语句意外变成了WHERE email = ? AND password = ? AND createdAt = 0。显然,额外的查询条件将导致错误的查询结果。

    使用findByExample()时,注意基本类型字段总是会加入到WHERE条件!

    使用Criteria查询

    第二种查询方法是使用Criteria查询,可以实现如下:

    1. public User login(String email, String password) {
    2. DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
    3. criteria.add(Restrictions.eq("email", email))
    4. .add(Restrictions.eq("password", password));
    5. List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
    6. return list.isEmpty() ? null : list.get(0);
    7. }

    DetachedCriteria使用链式语句来添加多个AND条件。和findByExample()相比,findByCriteria()可以组装出更灵活的WHERE条件,例如:

    1. SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?

    上述查询没法用findByExample()实现,但用Criteria查询可以实现如下:

    1. DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
    2. criteria.add(
    3. Restrictions.and(
    4. Restrictions.or(
    5. Restrictions.eq("email", email),
    6. Restrictions.eq("name", email)
    7. ),
    8. Restrictions.eq("password", password)
    9. )
    10. );

    只要组织好Restrictions的嵌套关系,Criteria查询可以实现任意复杂的查询。

    使用HQL查询

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

    1. 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类标注:

    1. @NamedQueries(
    2. @NamedQuery(
    3. // 查询名称:
    4. name = "login",
    5. // 查询语句:
    6. query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
    7. )
    8. )
    9. @Entity
    10. public class User extends AbstractEntity {
    11. ...
    12. }

    注意到引入的NamedQuery是javax.persistence.NamedQuery,它和直接传入HQL有点不同的是,占位符使用?0?1,并且索引是从0开始的(真乱)。

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

    1. public User login(String email, String password) {
    2. List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login", email, password);
    3. return list.isEmpty() ? null : list.get(0);
    4. }

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

    如果要使用Hibernate原生接口,但不知道怎么写,可以参考HibernateTemplate的源码。使用Hibernate的原生接口实际上总是从SessionFactory出发,它通常用全局变量存储,在HibernateTemplate中以成员变量被注入。有了SessionFactory,使用Hibernate用法如下:

    1. void operation() {
    2. Session session = null;
    3. boolean isNew = false;
    4. // 获取当前Session或者打开新的Session:
    5. try {
    6. session = this.sessionFactory.getCurrentSession();
    7. } catch (HibernateException e) {
    8. session = this.sessionFactory.openSession();
    9. isNew = true;
    10. }
    11. // 操作Session:
    12. try {
    13. User user = session.load(User.class, 123L);
    14. }
    15. finally {
    16. // 关闭新打开的Session:
    17. if (isNew) {
    18. session.close();
    19. }
    20. }
    21. }

    练习

    从下载练习:集成Hibernate (推荐使用快速下载)

    小结

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

    • DataSource;
    • LocalSessionFactory;
    • HibernateTransactionManager;

    集成Hibernate - 图1