ORM框架的发展历史与MyBatis的高级应用
一、ORM框架的发展历程
1. JDBC操作
1.1 JDBC操作的特点
最初的时候我们肯定是直接通过jdbc来直接操作数据库的,本地数据库我们有一张t_user表,那么我们的操作流程是
// 注册 JDBC 驱动
Class.forName("com.mysql.cj.jdbc.Driver");// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC", "root", "123456");// 执行查询
stmt = conn.createStatement();
String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = 1";
ResultSet rs = stmt.executeQuery(sql);// 获取结果集
while (rs.next()) {Integer id = rs.getInt("id");String userName = rs.getString("user_name");String realName = rs.getString("real_name");String password = rs.getString("password");Integer did = rs.getInt("d_id");user.setId(id);user.setUserName(userName);user.setRealName(realName);user.setPassword(password);user.setDId(did);System.out.println(user);
}
具体的操作步骤是,首先在pom.xml中引入MySQL的驱动依赖,注意MySQL数据库的版本
- Class.forName注册驱动
- 获取一个Connection对象
- 创建一个Statement对象
- execute()方法执行SQL语句,获取ResultSet结果集
- 通过ResultSet结果集给POJO的属性赋值
- 最后关闭相关的资源
这种实现方式首先给我们的感觉就是操作步骤比较繁琐,在复杂的业务场景中会更麻烦。尤其是我们需要自己来维护管理资源的连接,如果忘记了,就很可能造成数据库服务连接耗尽。同时我们还能看到具体业务的SQL语句直接在代码中写死耦合性增强。每个连接都会经历这几个步骤,重复代码很多,总结上面的操作的特点:
- 代码重复
- 资源管理
- 结果集处理
- SQL耦合
针对这些问题我们可以自己尝试解决下
1.2 JDBC优化1.0
针对常规jdbc操作的特点,我们可以先从代码重复和资源管理方面来优化,我们可以创建一个工具类来专门处理这个问题
public class DBUtils {private static final String JDBC_URL = "jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC";private static final String JDBC_NAME = "root";private static final String JDBC_PASSWORD = "123456";private static Connection conn;/*** 对外提供获取数据库连接的方法* @return* @throws Exception*/public static Connection getConnection() throws Exception {if(conn == null){try{conn = DriverManager.getConnection(JDBC_URL,JDBC_NAME,JDBC_PASSWORD);}catch (Exception e){e.printStackTrace();throw new Exception();}}return conn;}/*** 关闭资源* @param conn*/public static void close(Connection conn ){close(conn,null);}public static void close(Connection conn, Statement sts ){close(conn,sts,null);}public static void close(Connection conn, Statement sts , ResultSet rs){if(rs != null){try {rs.close();}catch (Exception e){e.printStackTrace();}}if(sts != null){try {sts.close();}catch (Exception e){e.printStackTrace();}}if(conn != null){try {conn.close();}catch (Exception e){e.printStackTrace();}}}
}
对应的jdbc操作代码可以简化如下
/**** 通过JDBC查询用户信息*/public void queryUser(){Connection conn = null;Statement stmt = null;User user = new User();ResultSet rs = null;try {// 注册 JDBC 驱动// Class.forName("com.mysql.cj.jdbc.Driver");// 打开连接conn = DBUtils.getConnection();// 执行查询stmt = conn.createStatement();String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = 1";rs = stmt.executeQuery(sql);// 获取结果集while (rs.next()) {Integer id = rs.getInt("id");String userName = rs.getString("user_name");String realName = rs.getString("real_name");String password = rs.getString("password");Integer did = rs.getInt("d_id");user.setId(id);user.setUserName(userName);user.setRealName(realName);user.setPassword(password);user.setDId(did);System.out.println(user);}} catch (SQLException se) {se.printStackTrace();} catch (Exception e) {e.printStackTrace();} finally {DBUtils.close(conn,stmt,rs);}}/*** 通过JDBC实现添加用户信息的操作*/public void addUser(){Connection conn = null;Statement stmt = null;try {// 打开连接conn = DBUtils.getConnection();// 执行查询stmt = conn.createStatement();String sql = "INSERT INTO T_USER(user_name,real_name,password,age,d_id)values('wangwu','王五','111',22,1001)";int i = stmt.executeUpdate(sql);System.out.println("影响的行数:" + i);} catch (SQLException se) {se.printStackTrace();} catch (Exception e) {e.printStackTrace();} finally {DBUtils.close(conn,stmt);}}
但是整体的操作步骤还是会显得比较复杂,这时我们可以进一步优化
1.3 JDBC优化2.0
我们可以针对DML操作的方法来优化,先解决SQL耦合的问题,在DBUtils中封装DML操作的方法
/*** 执行数据库的DML操作* @return*/public static Integer update(String sql,Object ... paramter) throws Exception{conn = getConnection();PreparedStatement ps = conn.prepareStatement(sql);if(paramter != null && paramter.length > 0){for (int i = 0; i < paramter.length; i++) {ps.setObject(i+1,paramter[i]);}}int i = ps.executeUpdate();close(conn,ps);return i;}
然后在DML操作的时候我们就可以简化为如下步骤
/*** 通过JDBC实现添加用户信息的操作*/public void addUser(){String sql = "INSERT INTO T_USER(user_name,real_name,password,age,d_id)values(?,?,?,?,?)";try {DBUtils.update(sql,"wangwu","王五","111",22,1001);} catch (Exception e) {e.printStackTrace();}}
显然这种方式会比最初的使用要简化很多,但是在查询处理的时候我们还是没有解决ResultSet结果集的处理问题,所以我们还需要继续优化
1.4 JDBC优化3.0
针对ResultSet的优化我们需要从反射和元数据两方面入手,具体如下
/*** 查询方法的简易封装* @param sql* @param clazz* @param parameter* @param <T>* @return* @throws Exception*/public static <T> List<T> query(String sql, Class clazz, Object ... parameter) throws Exception{conn = getConnection();PreparedStatement ps = conn.prepareStatement(sql);if(parameter != null && parameter.length > 0){for (int i = 0; i < parameter.length; i++) {ps.setObject(i+1,parameter[i]);}}ResultSet rs = ps.executeQuery();// 获取对应的表结构的元数据ResultSetMetaData metaData = ps.getMetaData();List<T> list = new ArrayList<>();while(rs.next()){// 根据 字段名称获取对应的值 然后将数据要封装到对应的对象中int columnCount = metaData.getColumnCount();Object o = clazz.newInstance();for (int i = 1; i < columnCount+1; i++) {// 根据每列的名称获取对应的值String columnName = metaData.getColumnName(i);Object columnValue = rs.getObject(columnName);setFieldValueForColumn(o,columnName,columnValue);}list.add((T) o);}return list;}/*** 根据字段名称设置 对象的属性* @param o* @param columnName*/private static void setFieldValueForColumn(Object o, String columnName,Object columnValue) {Class<?> clazz = o.getClass();try {// 根据字段获取属性Field field = clazz.getDeclaredField(columnName);// 私有属性放开权限field.setAccessible(true);field.set(o,columnValue);field.setAccessible(false);}catch (Exception e){// 说明不存在 那就将 _ 转换为 驼峰命名法if(columnName.contains("_")){Pattern linePattern = Pattern.compile("_(\\w)");columnName = columnName.toLowerCase();Matcher matcher = linePattern.matcher(columnName);StringBuffer sb = new StringBuffer();while (matcher.find()) {matcher.appendReplacement(sb, matcher.group(1).toUpperCase());}matcher.appendTail(sb);// 再次调用复制操作setFieldValueForColumn(o,sb.toString(),columnValue);}}}
封装了以上方法后我们的查询操作就可以简化为
/**** 通过JDBC查询用户信息*/public void queryUser(){try {String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = ?";List<User> list = DBUtils.query(sql, User.class,2);System.out.println(list);} catch (Exception e) {e.printStackTrace();}}
这样一来我们在操作数据库中数据的时候就只需要关注于核心的SQL操作了。当然以上的设计还比较粗糙,,这时Apache 下的 DbUtils是一个很好的选择
2.Apache DBUtils
官网地址:https://commons.apache.org/proper/commons-dbutils/
2.1 初始配置
DButils中提供了一个QueryRunner类,它对数据库的增删改查的方法进行了封装,获取QueryRunner的方式
private static final String PROPERTY_PATH = "druid.properties";private static DruidDataSource dataSource;private static QueryRunner queryRunner;public static void init() {Properties properties = new Properties();InputStream in = DBUtils.class.getClassLoader().getResourceAsStream(PROPERTY_PATH);try {properties.load(in);} catch (IOException e) {e.printStackTrace();}dataSource = new DruidDataSource();dataSource.configFromPropety(properties);// 使用数据源初始化 QueryRunnerqueryRunner = new QueryRunner(dataSource);}
创建QueryRunner对象的时候我们需要传递一个DataSource对象,这时我们可以选择Druid或者Hikai等常用的连接池工具,我这儿用的是Druid。
druid.username=root
druid.password=123456
druid.url=jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC
druid.minIdle=10
druid.maxActive=30
2.2 基本操作
QueryRunner中提供的方法解决了重复代码的问题,传入数据源解决了资源管理的问题。而对于ResultSet结果集的处理则是通过 ResultSetHandler 来处理。我们可以自己来实现该接口
/*** 查询所有的用户信息* @throws Exception*/public void queryUser() throws Exception{DruidUtils.init();QueryRunner queryRunner = DruidUtils.getQueryRunner();String sql = "select * from t_user";List<User> list = queryRunner.query(sql, new ResultSetHandler<List<User>>() {@Overridepublic List<User> handle(ResultSet rs) throws SQLException {List<User> list = new ArrayList<>();while(rs.next()){User user = new User();user.setId(rs.getInt("id"));user.setUserName(rs.getString("user_name"));user.setRealName(rs.getString("real_name"));user.setPassword(rs.getString("password"));list.add(user);}return list;}});for (User user : list) {System.out.println(user);}}
或者用DBUtils中提供的默认的相关实现来解决
/*** 通过ResultHandle的实现类处理查询*/public void queryUserUseBeanListHandle() throws Exception{DruidUtils.init();QueryRunner queryRunner = DruidUtils.getQueryRunner();String sql = "select * from t_user";// 不会自动帮助我们实现驼峰命名的转换List<User> list = queryRunner.query(sql, new BeanListHandler<User>(User.class));for (User user : list) {System.out.println(user);}}
通过Apache 封装的DBUtils是能够很方便的帮助我们实现相对比较简单的数据库操作
3.SpringJDBC
在Spring框架平台下,也提供的有JDBC的封装操作,在Spring中提供了一个模板方法 JdbcTemplate,里面封装了各种各样的 execute,query和update方法。
JdbcTemplate这个类是JDBC的核心包的中心类,简化了JDBC的操作,可以避免常见的异常,它封装了JDBC的核心流程,应用只要提供SQL语句,提取结果集就可以了,它是线程安全的。
3.1 初始配置
在SpringJdbcTemplate的使用中,我们依然要配置对应的数据源,然后将JdbcTemplate对象注入到IoC容器中。
@Configuration
@ComponentScan
public class SpringConfig {@Beanpublic DataSource dataSource(){DruidDataSource dataSource = new DruidDataSource();dataSource.setUsername("root");dataSource.setPassword("123456");dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisdb?characterEncoding=utf-8&serverTimezone=UTC");return dataSource;}@Beanpublic JdbcTemplate jdbcTemplate(DataSource dataSource){JdbcTemplate template = new JdbcTemplate();template.setDataSource(dataSource);return template;}
}
3.2 CRUD操作
在我们具体操作数据库中数据的时候,我们只需要从容器中获取JdbcTemplate实例即可
@Repository
public class UserDao {@Autowiredprivate JdbcTemplate template;public void addUser(){int count = template.update("insert into t_user(user_name,real_name)values(?,?)","bobo","波波老师");System.out.println("count = " + count);}public void query1(){String sql = "select * from t_user";List<User> list = template.query(sql, new RowMapper<User>() {@Overridepublic User mapRow(ResultSet rs, int rowNum) throws SQLException {User user = new User();user.setId(rs.getInt("id"));user.setUserName(rs.getString("user_name"));user.