阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

集成Hibernate

25次阅读
没有评论

共计 8572 个字符,预计需要花费 22 分钟才能阅读完成。

使用 JdbcTemplate 的时候,我们用得最多的方法就是 List<T> 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 中讲到过 FactoryBeanLocalSessionFactoryBean 是一个 FactoryBean,它会再自动创建一个SessionFactory,在 Hibernate 中,Session 是封装了一个 JDBC Connection的实例,而 SessionFactory 是封装了 JDBC DataSource的实例,即 SessionFactory 持有连接池,每次需要操作数据库的时候,SessionFactory创建一个新的 Session,相当于从连接池获取到一个新的ConnectionSessionFactory 就是 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 语句是否符合我们的预期。

除了设置 DataSourceProperties之外,注意到 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是自增主键,emailpasswordnameVARCHAR 类型,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指示列是否允许为 NULLupdatable 指示该列是否允许被用在 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() {...}
}

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

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

@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,我们就可以大幅简化UserBook

@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 配置。

类似 UserBook 这样的用于 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 来删除记录,因此,要正确设置 Userid属性才能正常删除记录:

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 语句中,这样可以提供一层额外的安全性,即如果不小心修改了 UseremailcreatedAt等属性,执行 update() 时并不会更新对应的数据库列。但也必须牢记:这个功能是 Hibernate 提供的,如果绕过 Hibernate 直接通过 JDBC 执行 UPDATE 语句仍然可以更新数据库的任意列的值。

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

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

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

使用 HQL 查询

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

List<User> 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 {...}

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

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

public User login(String email, String password) {List<User> 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。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计8572字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中