知己知彼:持久化演进

引言

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

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

image.png

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());
    } finally {
        statement.close();
        connection.close();
    }
}

核心的功能其实就一行: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获取查询结果,这里为什么Spring封装的query方法不直接返回ResultSet对象,而要设计这样一个回调的对象呢?

提示,JdbcTemplate查询的核心源码如下:

@Override
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
    ResultSet rs = null;
    try {
        rs = stmt.executeQuery(sql);
        return rse.extractData(rs);
    }
    finally {
        JdbcUtils.closeResultSet(rs);
    }
}

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,毕竟这么多公司使用,肯定有他们的道理,如果足够优秀,也可以考虑使用。可是结果却有些令人失望。

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

image.png

官网的例子不够详细,我又参阅了许多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

image.png

最终的讨论结果如下:

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

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

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

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

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

总结

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

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

阅读 580

推荐阅读
Tomorrow
用户专栏

明天,你好

319 人关注
137 篇文章
专栏主页