下面内容转载和参考自:集成Hibernate - 廖雪峰的官方网站
1、Hibernate
前面说过,在Spring JDBC中可以使用JdbcTemplate的queryForObject()/query()方法配合RowMapper来将查询的结果映射为Java Bean,这种把数据库的表中的记录映射为Java对象的过程就是ORM:Object-Relational Mapping,ORM既可以把记录转换成Java对象(从数据库读取数据),也可以把Java对象转换为行记录(向数据库写数据)。使用Spring JDBC的JdbcTemplate实现ORM是最原始的ORM,可以选择更高级的ORM框架,如Hibernate。
①、配置定义
使用Hibernate,在Maven中需要加入以下依赖:
<!--Spring ORM-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.2.Final</version>
</dependency>
类似Spring JDBC中的做法,使用Hibernate也需要一个DataSource,同时需要开启声明式事务。使用Hibernate之前需要提供创建LocalSessionFactoryBean的Bean方法,LocalSessionFactoryBean是一个FactoryBean,它会自动创建一个SessionFactory,SessionFactory会使用DataSource。也就是说SessionFactory是封装了DataSource的实例,即SessionFactory持有JDBC连接池,每次需要操作数据库的时候,SessionFactory创建一个新的Session来代表一个Connection。另外还需要创建HibernateTemplate以及HibernateTransactionManager的Bean方法。
@Configuration
@ComponentScan
@EnableTransactionManagement //启用声明式事务
@PropertySource("jdbc.properties")
public class AppConfig {
@Value("${jdbc.url}")
String jdbcUrl;
@Value("${jdbc.username}")
String jdbcUsername;
@Value("${jdbc.password}")
String jdbcPassword;
@Bean
DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(jdbcUsername);
config.setPassword(jdbcPassword);
config.addDataSourceProperty("autoCommit", "true");
config.addDataSourceProperty("connectionTimeout", "5");
config.addDataSourceProperty("idleTimeout", "60");
return new HikariDataSource(config);
}
@Bean //提供给Hibernate创建LocalSessionFactoryBean对象的方法
LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource) {
var sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
// 扫描指定的package获取所有entity class:
sessionFactoryBean.setPackagesToScan("xsl.package"); //传入一个package名称,Hibernate在该包下自动搜索能映射为数据库表记录的JavaBean
//用Properties持有Hibernate初始化SessionFactory时用到的所有设置
var props = new Properties();
props.setProperty("hibernate.hbm2ddl.auto", "update"); //自动创建数据库的表结构(生产环境下不要使用该选项)
props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); //指示Hibernate使用的数据库是HSQLDB,Hibernate使用HQL语句,它类似SQL,Hibernate会根据这里设定的数据库来生成针对数据库优化的SQL
props.setProperty("hibernate.show_sql", "true"); //打印执行的SQL,这对于调试非常有用
sessionFactoryBean.setHibernateProperties(props);
return sessionFactoryBean;
}
@Bean //提供给Hibernate创建HibernateTemplate的方法,使用该HibernateTemplate对象来进行Hibernate相关操作(HibernateTemplate是Spring为了便于我们使用Hibernate提供的工具类)
HibernateTemplate createHibernateTemplate(@Autowired SessionFactory sessionFactory) {
return new HibernateTemplate(sessionFactory);
}
@Bean //提供给Hibernate创建PlatformTransactionManager(事务管理器)的方法,用以使用声明式事务
PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory) {
return new HibernateTransactionManager(sessionFactory);
}
}
②、设置JavaBean
Hibernate相关的配置定义完毕后,就可以通过它来进行数据库操作,在这之前,我们还需要告诉Hibernate如何把JavaBean类映射成表中记录。比如一个user数据库表,其一条记录用JavaBean类User表示如下,我们需要使用@Entity、@Id、@Column等注解来告诉Hibernate如何把User类映射到表记录。@Entity、@Id等这些注解均来自javax.persistence,它是JPA规范的一部分,类似下面User这样的用于ORM的Java Bean,我们通常称之为Entity Bean。
需要注意的是,使用Hibernate时,不要使用int等基本类型的属性,总是使用包装类型,如Integer或Long,因为Hibernate中很多地方会根据值是否为null做对应的处理,而基本类型会被赋默认值,这就会出现与期待不一致的相关问题。
@Entity //标识这个JavaBean被用于映射
@Table(name="users") //默认情况下User类映射的数据库表名是user,这里通过@Table注解来另指定表名为users
public class User {
private Long id; //使用包装类型
private String email;
private String password;
private String name;
private Long createdAt; //时间戳,表示创建时间
// getters and setters
@Id //指定id为主键
@GeneratedValue(strategy = GenerationType.IDENTITY) //指定id为自增类型
@Column(nullable = false/*id是否允许为NULL*/, updatable = false/*id是否允许被用在UPDATE语句*/)
public Long getId() { ... }
@Column(nullable = false, unique = true/*email带值唯一索引*/, length = 100 /*email的长度(String类型),默认是255*/)
public String getEmail() { ... }
......
@Transient //指示该方法不是JavaBean的getters/setters方法
public ZonedDateTime getCreatedDateTime() {
return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
}
@PrePersist //在JavaBean持久化到数据库之前(即执行INSERT语句之前),Hibernate会先执行该方法
public void preInsert() {
setCreatedAt(System.currentTimeMillis()); //设置好createdAt属性的值
}
}
③、使用HibernateTemplate进行SQL操作
使用Hibernate进行SQL操作,实际上是对JavaBean进行“增删改查”,我们通过HibernateTemplate 来实现这些操作,所以注入一个HibernateTemplate:
@Component
@Transactional
public class UserService {
@Autowired
HibernateTemplate hibernateTemplate;
//Insert操作:向users表插入一条记录
public User register(String email, String password, String name) {
User user = new User();
// 不要设置id,因为使用了自增主键
user.setEmail(email);
user.setPassword(password);
user.setName(name);
//创建时间createdAt是在执行insert之前自动设置的
hibernateTemplate.save(user); //insert到数据库
System.out.println(user.getId()); //打印自动获得的id
return user;
}
//Update操作:更新指定的属性,通过id,对于标注了@Column(updatable=false)的属性不会进行更新
public void updateUser(Long id, String name) { //更新id为指定值记录的name属性
User user = hibernateTemplate.load(User.class, id); //根据主键id加载指定记录,记录不存在的话抛出异常
user.setName(name); //更新name属性
hibernateTemplate.update(user); //执行更新操作
}
//Delete操作:使用id来删除指定的记录
public boolean deleteUser(Long id) {
User user = hibernateTemplate.get(User.class, id); //根据id加载指定的记录,记录不存在的话返回null
if (user != null) {
hibernateTemplate.delete(user); //执行删除操作
return true;
}
return false;
}
}
要执行查询操作,有四种方法,比如想要执行“SELECT * FROM user WHERE email = ? AND password = ?”的查询:
@Component
@Transactional
public class UserService {
...
//查询方法1:使用Example
public User login(String email, String password) {
User example = new User();
example.setEmail(email);
example.setPassword(password);
//Hibernate会把User实例所有非null的属性拼成WHERE条件,这里即是email和password
List<User> list = hibernateTemplate.findByExample(example); //获得查询结果到List
return list.isEmpty() ? null : list.get(0);
}
//查询方法2:使用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));//DetachedCriteria使用链式语句来添加多个AND条件
List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria); //获得查询结果到List
return list.isEmpty() ? null : list.get(0);
}
//查询方法3:编写Hibernate内置的HQL语言查询
public User login(String email, String password){
List<User> list = (List<User>) hibernateTemplate.find("FROM User WHERE email=? AND password=?",
email, password);//获得查询结果到List
return list.isEmpty() ? null : list.get(0);
}
}
第四种方法是使用NamedQuery(javax.persistence.NamedQuery),它给查询语句起个名字,然后将查询语句和名称保存在JavaBean的注解中,在查询方法中通过查询名来获得查询语句:
@NamedQueries(
@NamedQuery(
name = "login", //设置查询名称为"login"
query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1" //要执行的查询语句,占位符使用?0、?1
)
)
@Entity
@Table(name="users")
public class User {
...
}
@Component
@Transactional
public class UserService {
...
//查询方法4:使用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);
}
}
使用第二种查询方法可以组装出更灵活的WHERE条件,例如实现“SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?”:
public User login(String email, String name, String password) {
DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
criteria.add(
Restrictions.and(
Restrictions.or(
Restrictions.eq("email", email),
Restrictions.eq("name", name)
),
Restrictions.eq("password", password)
)
);
}
还可以使用Hibernate的原生接口来自己手动对SessionFactory、Session仅限操作来实现SQL操作,具体可以参考HibernateTemplate的源码。
2、JPA
Java EE中其实包含ORM标准,JPA(Java Persistence API)就是这样的一个标准,所以我们除了使用Hibernate这种第三方包之外,还可以使用实现了JPA接口的包(就像MySQL驱动实现了JDBC接口一样)来进行SQL操作。可以选择EclipseLink作为JPA的实现,但Hibernate实际上也实现了JPA接口,所以也可以选择Hibernate作为JPA的底层实现。使用JPA来进行数据库操作的话,具体可以参考:集成JPA - 廖雪峰的官方网站。
3、MyBatis
①、ORM框架内部实现
针对一对多关系(比如一个人有多个住宅地址,那么住宅信息表中的外键id就指向用户表中的主键id),在Hibernate中可以直接通过代理类的getter方法来查询数据库,其实现原理如下。getAddress()中为使用Hibernate的原生接口来进行SQL操作,使用Hibernate的原生接口实际上总是从SessionFactory出发,然后使用Session等进行操作,具体可以参考HibernateTemplate的源码:
②、MyBatis的由来
使用Hibernate或JPA会有以下三个缺点:
A、状态切换
一个JavaBean对象在Hibernate中有三种状态:a、临时态,不在Session的缓存当中,在数据库中没有对应的记录,比如刚创建的对象。b、持久化态,已经加入到Session的缓存中,在数据库中有对应的记录,比如调用sava()后的对象。c、游离态,不在session缓存中,但数据库有与之对应的记录,比如delete()之后的对象。不同的操作使对象的状态不断改变,这使得普通Java Bean的生命周期变得复杂,不了解对象的状态而进行错误的操作的话,会造成大量的PersistentObjectException异常。
B、内置查询语言
Hibernate和JPA为了实现兼容多种数据库,它使用HQL或JPQL查询语言,经过内部转换,变成MySQL等特定数据库的SQL,理论上这样可以做到无缝切换数据库,但这一层内部转换除了少许的性能开销外,给SQL级别的优化带来了麻烦。
C、二级缓存
ORM框架通常提供了缓存,分为一级缓存和二级缓存。一级缓存是指在一个Session范围内的缓存,比如下面的两次根据同一主键进行的查询操作,因为有缓存,所以返回的是同一对象实例:
//使用Hibernate内置接口进行操作
long id = 123;
User user1 = session.load(User.class, id);
User user2 = session.load(User.class, id);
二级缓存是指跨Session的缓存(默认二级缓存是关闭的),比如A线程使用下面的语句来获得一个实例对象,然后B线程也使用同样的语句获得一个实例对象,当二级缓存开启后,两个线程获得的对象实例是一个。但是这里有个问题,如果A线程获得对象实例后,然后使用语句“UPDATE users SET address = "" WHERE createdAt <= ?”更新了id为123的数据库记录,但ORM中无法判断id为123的JavaBean对象是否受该语句影响,因为该语句没有使用id进行操作,所以这之后线程B获得还是缓存中的对象,与线程A执行UPDATE操作之前获取的对象是同一个。
User user1 = session1.load(User.class, 123);
像Hibernate这种ORM框架称之为全自动ORM框架,它省去了很多Spring JdbcTemplate中的操作,直接对JavaBean进行操作就可以进行SQL操作,但有状态切换和缓存等问题。而使用Spring JdbcTemplate的话需要我们手动编写和构造SQL语句,以及手动将ResultSet转换为JavaBean对象(通过提供Mapper实例),虽然代码有点繁琐但是没有缓存等问题,即每次读取操作一定是数据库操作而不是缓存。还有一种半自动的ORM,它负责ResultSet和Java Bean之间的映射,但我们需要提供SQL语句,MyBatis就是这样一种半自动化ORM框架。
③、集成MyBatis
相关依赖:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.version>5.2.0.RELEASE</spring.version>
<mybatis.version>3.5.4</mybatis.version>
<mybatis-spring.version>2.0.4</mybatis-spring.version>
<hikaricp.version>3.4.2</hikaricp.version>
<hsqldb.version>2.5.0</hsqldb.version>
</properties>
<dependencies>
<!--Spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring ORM-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!--集成MyBatis与Spring的库-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!--JDBC连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<!--JDBC驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.9-rc</version>
</dependency>
</dependencies>
类似Hibernate,使用MyBatis也需要一个DataSource,同时也需要开启声明式事务,如下在Ioc配置类中进行相关的设置。MyBatis的SqlSessionFactory和SqlSession对应Hibernate的SessionFactory和Session,即SqlSessionFactory封装了DataSource,其内部持有JDBC连接池,操作数据库的时候,SqlSessionFactory创建一个新的SqlSession来代表一个JDBC Connection。
如果运行程序提示有异常信息 “You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.” 的话,是因为安装mysql的时候时区设置的不正确,可以在jdbc.url后面加上serverTimezone=GMT即可解决问题,如果需要使用gmt+8时区,需要写成GMT%2B8,否则会被解析为空。
# jdbc.properties
# 地址、端口、数据库名、附加参数
jdbc.url=jdbc:mysql://127.0.0.1:3306/database_name?useUnicode=true&characterEncoding=UTF-8&useSSL=false
# 用户名、密码
jdbc.username=sa
jdbc.password=123456
@Configuration
@ComponentScan
@EnableTransactionManagement //启用声明式事务
@PropertySource("jdbc.properties")
public class AppConfig {
@Value("${jdbc.url}")
String jdbcUrl;
@Value("${jdbc.username}")
String jdbcUsername;
@Value("${jdbc.password}")
String jdbcPassword;
@Bean
DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(jdbcUsername);
config.setPassword(jdbcPassword);
config.addDataSourceProperty("autoCommit", "true");
config.addDataSourceProperty("connectionTimeout", "5");
config.addDataSourceProperty("idleTimeout", "60");
return new HikariDataSource(config);
}
//提供给MyBatis创建SqlSessionFactoryBean对象的方法
@Bean
SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource) {
var sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
//提供给MyBatis创建PlatformTransactionManager(事务管理器)的方法,用以使用声明式事务
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
MyBatis使用接口来实现映射,我们可以自己来实现这个接口,也可以使用@MapperScan注解来让MyBatis自动实现接口类:
class User {
private Long id;
private String email;
private String password;
private String name;
private Long createdAt; //时间戳,表示创建时间
// getters and setters
...
//非getters and setters方法需要使用@Transient注解声明
@Transient
public void func() {
}
...
}
//User类和数据库users表之间映射的Mapper
public interface UserMapper {
//SELECT查询方法
@Select("SELECT * FROM users WHERE id = #{id}") //getById()方法里对应执行的SQL语句
User getById(@Param("id") long id); //通过主键获得一行记录的方法,@Param指示该参数与SQL语句中"id"参数对应
@Select("SELECT id, name, created_time AS createdAt FROM users LIMIT #{offset}, #{maxResults}") //User类成员名要与查询记录的列名相同,列名和类属性名不同的话使用AS
List<User> getAllLimit(@Param("offset") int offset, @Param("maxResults") int maxResults);
//INSERT插入方法
//如果表的id是自增主键,那么,我们在SQL中不传入id,但希望获取插入后的主键,需要再加一个@Options注解
@Options(useGeneratedKeys = true/*设置自增主键*/, keyProperty = "id"/*主键值使用JavaBean的id属性值*/, keyColumn = "id"/*设置主键列名*/)
@Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})")
void insert(@Param("user") User user);
//UPDATE更新方法
@Update("UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}")
void update(@Param("user") User user);
//DELETE删除方法
@Delete("DELETE FROM users WHERE id = #{id}")
void deleteById(@Param("id") long id);
}
@Configuration
@ComponentScan
@EnableTransactionManagement
@MapperScan("xsl") //自动定义xsl包下接口的实现类
@PropertySource("jdbc.properties")
public class AppConfig {
...
}
④、使用MyBatis
下面为使用MyBatis进行SQL插入操作,在执行之前可以先创建数据库和数据库表,如下所示设置了email列的非重约束,插入数据的时候包含重复的email的话程序可能会抛出异常(查询或插入的时候列名不对的话在Java中也会抛出异常):
create database my_database;
use my_database;
CREATE TABLE IF NOT EXISTS users (
id BIGINT auto_increment NOT NULL PRIMARY KEY,
email VARCHAR(100) NOT NULL,
password VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
createdAt BIGINT NOT NULL,
UNIQUE (email));
@Component
@Transactional
public class UserService {
// 注入UserMapper: UserMapper的实现类已经被MyBatis自动定义(通过@MapperScan注解)
@Autowired
UserMapper userMapper;
public void test() {
long id = 123;
User user = userMapper.getById(id);
if (user == null) {
throw new RuntimeException("User not found by id.");
}
}
}
对于数据库操作中出现的错误,比如执行插入的时候表不存在,对于值唯一列插入了相同的元素等,会抛出相关的异常,所以调用Mapper接口中方法时,可以添加try-catch处理。
MyBatis也允许使用XML配置映射关系和SQL语句,比如要使用MyBatis创建数据库表的话,可以使用XML来配置映射,具体可以参考其官方文档。
因为MyBatis使用SQL语句,而不是像Hibernate那样使用内置的HQL然后转换成各家SQL语言,所以使用MyBatis的话对于以后要切换数据库的话就不太容易。另外MyBatis也没有Hibernate中自动加载一对多的功能。
4、总结
使用Spring JDBC进行查询的话,如果JavaBean的属性名(成员名)与数据库表中列名相同的话,那么查询结果就可以直接保存到JavaBean中。
使用Hibernate的话不仅可以将查询到的记录直接使用Java对象保存,还可以将Java对象直接转换为数据库记录,使用Hibernate进行数据库操作的话不需要编写SQL语句,使用HibernateTemplate直接对Java对象进行相关操作即可。Hibernate读取和写入数据库都不需要编写SQL语句, 它提供了数据库记录和JavaBean对象相互转换的全自动映射,所以属于全自动的ORM。 Hibernate不用编写SQL语句,直接操作JavaBean对象就相当于操作数据库,这也带了了一些问题,可以使用MyBatis替换它。
MyBatis是一种半自动的ORM,它能够将查询结果自动映射到Java Bean,也可以通过JavaBean来更新数据库,但是需要自己编写SQL语句。