Hibernate和MyBatis【转】

下面内容转载和参考自:集成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语句。

  

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值