乐趣区

知己知彼持久化演进

引言

谈论起“持久化”一词,天天操作数据库的大家肯定不会陌生。

持久化中,备受开发者瞩目的就是两大巨头:HibernateMyBatis,许多开发者也常常拿二者对比。去Google 上,都是类似两者互相对比的文章。

HibernateMyBatis,就像javaphp一样,双方似有打架之势。

虽然我是使用 Hibernate 的工程师,但是我并不否认MyBatis,两者都优秀,但一切都是业务优先。

学习了潘老师的《SpringBoot+Angular 入门实例教程》后,也想到了最原始的JDBC

今天,我们就好好地从古至今,聊聊持久化技术的演进与对比。

持久化

远古的 JDBC

JDBCJava Database Connectivity,简称 JDBC。是Java 语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC是面向关系型数据库的。

每一位 Java 程序员可能都经历过被 JDBC 支配的恐惧。

下面是我在 Java 实验课上写过的 JDBC 代码,实现的功能很简单,就是要将一个 Student 对象保存到数据库中的 student 表中。

/**
 * 持久化学生实体
 */
private static void persistStudent(Student student) {
    try {Class.forName("com.mysql.jdbc.Driver");
        // 数据库连接配置
        String url = "jdbc:mysql://127.0.0.1:7777/java?characterEncoding=utf-8";
        // 获取连接
        Connection connection = DriverManager.getConnection(url, "root", "root");
        // 获取语句
        Statement statement = connection.createStatement();
        // 生成 SQL 语句
        String SQL = student.toSQLString();
        // 执行语句
        statement.executeUpdate(SQL);
    } catch (SQLException e) {System.out.println("ERROR:" + e.getMessage());
    } catch (ClassNotFoundException e) {System.out.println("ERROR:" + e.getMessage());
    }
}

核心的功能其实就一行:statement.executeUpdate(SQL),我只想执行一条 INSERT 语句保证数据的持久化。

可是在 JDBC 中却需要实现加载 MySQL 驱动,获取Connection,获取Statement,才能执行SQL,执行之后还需要手动释放资源,不可谓不麻烦。

程序员最讨厌写重复的代码,就像我感觉从华软平台移植重复代码到试题平台很枯燥一样,所以这些“模版式”的代码需要封装。

封装工具类

我们都是平凡人,我们能想到的,肯定早就有人想到了。

Spring封装了JDBC,提供了JdbcTemplate

另一个比较出名的是 Apache 封装的DBUtils

使用了 JdbcTemplate 后,我们无需再编写模版式的 ConnectionStatementtry ... catch ... finally 等代码。教程中使用了 JdbcTemplate 查询教师表,代码长这样:

@GetMapping
public List<Teacher> getAll() {
    /* 初始化不固定大小的数组 */
    List<Teacher> teachers = new ArrayList<>();

    /* 定义实现了 RowCallbackHandler 接口的对象 */
    RowCallbackHandler rowCallbackHandler = new RowCallbackHandler() {
        /**
         * 该方法用于执行 jdbcTemplate.query 后的回调,每行数据回调 1 次。比如 Teacher 表中有两行数据,则回调此方法两次。*
         * @param resultSet 查询结果,每次一行
         * @throws SQLException 查询出错时,将抛出此异常,暂时不处理。*/
        @Override
        public void processRow(ResultSet resultSet) throws SQLException {Teacher teacher = new Teacher();
            /* 获取字段 id,并转换为 Long 类型返回 */
            teacher.setId(resultSet.getLong("id"));
            /* 获取字段 name,并转换为 String 类型返回 */
            teacher.setName(resultSet.getString("name"));
            /* 获取字段 sex,并转换为布尔类型返回 */
            teacher.setSex(resultSet.getBoolean("sex"));
            teacher.setUsername(resultSet.getString("username"));
            teacher.setEmail(resultSet.getString("email"));
            teacher.setCreateTime(resultSet.getLong("create_time"));
            teacher.setUpdateTime(resultSet.getLong("update_time"));
            
            /* 将得到的 teacher 添加到要返回的数组中 */
            teachers.add(teacher);
        }
    };

    /* 定义查询字符串 */
    String query = "select id, name, sex, username, email, create_time, update_time from teacher";

    /* 使用 query 进行查询,并把查询的结果通过调用 rowCallbackHandler.processRow()方法传递给 rowCallbackHandler 对象 */
    jdbcTemplate.query(query, rowCallbackHandler);
    return teachers;
}

ResultSet用起来很讨厌是不是?需要手动地去获取字段并设置到对象中,JdbcTemplate提高了开发效率,但是提高地不明显,能不能再简单一点呢?

ORM

为了避免编写大量这样的与业务无关的代码,设计了 ORM 思想。

teacher.setName(resultSet.getString("name"));
teacher.setSex(resultSet.getBoolean("sex"));
teacher.setUsername(resultSet.getString("username"));
teacher.setEmail(resultSet.getString("email"));

ORM:即对象关系映射。将对象与数据表进行关联,我们无需关注这类不关联业务的冗余代码,操作对象,即操作数据表。

半自动化 ORM

半自动化的 ORM 框架,就是炙手可热的MyBatis

我简单地去学习了以下MyBatis,毕竟这么多公司使用,肯定有他们的道理,如果足够优秀,也可以考虑使用。可是结果却有些令人失望。

打开官网学习,这应该算是我见过的最寒酸的著名开源框架的官网了,里面的内容也不详细。

官网的例子不够详细,我又参阅了许多 MyBatis 的博文进行学习。

开启数据库字段下划线到对象命名驼峰的配置。

mybatis.configuration.map-underscore-to-camel-case=true

还是经典的教师、班级、学生的关系:

public class Teacher {

    private Long id;

    private String name;

    private Boolean sex;

    private String username;

    private String email;

    private Long createTime;

    private Long updateTime;
}

public class Klass {

    private Long id;

    private String name;

    private Teacher teacher;

    private List<Student> students;
}

public class Student {

    private Long id;

    private String name;
}

教师的 CRUD 单表查询:

@Mapper
public interface TeacherMapper {@Select("SELECT * FROM teacher")
    List<Teacher> getAll();

    @Select("SELECT * FROM teacher WHERE id = #{id}")
    Teacher get(Long id);

    @Select("SELECT * FROM teacher WHERE username = #{username}")
    Teacher findByUsername(String username);

    @Insert("INSERT INTO teacher(name, sex, username, email, create_time, update_time) VALUES(#{name}, #{sex}, #{username}, #{email}, #{createTime}, #{updateTime})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insert(Teacher teacher);

    @Update("UPDATE teacher SET name=#{name}, sex=#{sex}, email=#{email}, update_time=#{updateTime} WHERE id=#{id}")
    void update(Teacher teacher);

    @Delete("DELETE FROM teacher WHERE id=#{id}")
    void delete(Long id);
}

关联查询:

@Mapper
public interface KlassMapper {@Select("SELECT * FROM klass")
    @Results({@Result(column = "teacher_id", property = "teacher", one = @One(select = "club.yunzhi.mybatis.mapper.TeacherMapper.get")),
            @Result(column = "id", property = "students", many = @Many(select = "club.yunzhi.mybatis.mapper.StudentMapper.getAllByKlassId"))
    })
    List<Klass> getAll();}

关联中用到的 StudentMapper 子查询:

@Mapper
public interface StudentMapper {@Select("SELECT * FROM student WHERE klass_id=#{klassId}")
    List<Student> getAllByKlassId(Long klassId);
}

也学了一个晚上了,虽然二级缓存之类的高级特性还没学习呢,但是对 MyBatis 也算是又一个大体的了解了。

业务是老大,所有技术都是服务于业务的。程序员应该关注业务与实际问题,我是不太喜欢去写 SQL 的,SQL应该是 DBA 去专业学习并优化的,MyBatis看起来更像是给 DBA 用的框架一样。

全自动化 ORM

当一个系统设计完成之后,其他的工作就是搬砖。

搬砖当然是越简单越好,不编写 SQLHibernate 走起。

public interface TeacherRepository extends CrudRepository<Teacher, Long> {}

完全基于对象,更加注重业务。

怎么选?

好多人都是“你看支付宝用MyBatis,那我也用MyBatis”。

这是 StackOverflow 上一个十年前的关于两者对比讨论的话题:https://stackoverflow.com/questions/1984548/hibernate-vs-ibatis

最终的讨论结果如下:

iBatisHibernate 是完全不同的事物 (iBatisMyBatis的前身)。

如果以对象为中心,那 Hibernate 更好;如果以数据库为中心,那 iBatis 更好。

如果你设计系统架构,没有高并发的需求,Hibernate最合适,对象模型会使得代码非常简洁,但代价巨大。

如果你接手了一个遗留下来的数据库,并且需要编写复杂的 SQL 查询,那 iBatis 更合适。

总结一句话就是性能问题:Hibernate方便,但是会有性能损耗;MyBatis有性能优势。

总结

都说 MyBatis 适合高并发,但高并发又岂是一个 MyBatis 就能囊括的?

业务是老大,如果真的碰到了高并发需求,那又是我们进步的时候了。

退出移动版