你真的了解Java的数据驱动吗:从JDBC说起

数据库访问

我们要写数据库驱动程序,至少得先访问到、连接上数据库吧。

假设我们要写个连接数据库的应用程序,无论是我们要连接本地的数据库,还是远程的数据库,我们要做的事儿,无非就是进程间通信,我们靠OS提供给我们的socket就行啦,这时我们只需要Java和数据库定义一个应用层的协议, 就是所谓的你发什么请求, 我给你什么响应(例如:握手、认证、约定格式等)就行了。


接口的统一

MySQL、Oracle、SQL Server、DB2等等各家数据库,都有自己家的一个应用层访问协议,这就造成一个问题,我们的数据库连接程序只能对应一个数据库,要是从MySQL换到Oracle的话,就GG了,只能重写一套,这是相当麻烦的!!!

更难受的是, 每套代码都得处理非常多的协议细节, 我要是就只是写那么简简单单的一个SQL语句,也要写一堆杂七杂八的!!!问题的关键就在于:直接使用socket编程, 太low 了 , 必须得有一个抽象层来屏蔽这些细节!

于是乎Java想出了Connection这么个抽象玩意儿来代表连接,Statement来表示SQL语句,ResultSet 表示返回结果。并且,他们都是接口!!!具体怎么实现,按照具体数据库来,而其中那些实现的代码就需要处理那些烦人的细节了!!!

于是乎,这个玩意儿,就被叫做JDBC,Java自己定义一个标准化接口,丢出去,活让别人干去


面向接口编程

假的抽象编程

有了接口了,数据库也给咱提供具体实现的jar包了,那咱们怎么个写法呢?

Connection conn = new MysqlConnectionImpl("localhost","3306","stu_db","root","admin");

要是这么写,那就糟糕了啊!

?看起来没什么问题啊?很有问题啊!

要是我jar包升级,并且把类名改为MysqlConnectionJDBC4Impl咋办?那代码不就GG了,这哪是面向接口编程啊,这不还是面向具体吗,没碰到本质问题上。

想想设计模式,我们是不是应该把对象创建的具体实现封装起来,别让用户自己new!想想这对应什么模式呢?——工厂模式


新的一层抽象

Java冥思苦想,类比了一下我们的计算机,I/O设备都需要驱动才能使用,那我能不能把数据库也当成I/O设备,抽象出一个驱动作为中间层呢?我们来模拟一下:(Properties是一个配置类)

public class Driver{
    public static Connection getConnection(String dbType,Properties info){
        if("mysql".equals(dbType)){
            return new MysqlConnectionImpl(info);
        }
        if("oracle".equals(dbType)){
            return new OracleConnectionImpl(info);
        }
        if("db2".equals(dbType)){
            return new DB2ConnectionImpl(info);
        }
        throw new RuntimeException("unsupported db type = " + dbType);
    }
}

我们用简单工厂实现了,那我们再来看看怎么用吧!

Properties info = new Properties();
info.put("host","localhost");
info.put("port","3306");
// 配置一堆玩意儿...
Connection conn = Driver.getConnection("mysql",info); 

这不就拿到Connection接口了吗?面向抽象,永远嘀神!

等等,不对啊,问题还是没解决啊,我如果要增加数据库,或者修改连接类的类名,还是要去改Driver的代码啊喂,那咋办嘛?


数据驱动

为了实现彻底解耦,我们可以把数据库驱动所需class的全限定类名写在配置文件里,通过I/O去读配置文件内容就好啦!这样就不用去修改代码了,直接修改配置文件就行,不然程序还得重新编译运行,头疼啊!

mysql = com.mysql.jdbc.MysqlConnectionImpl
db2 = com.ibm.db2.DB2ConnectionImpl
oracle = com.oracle.jdbc.OracleConnection
sqlserver = com.Microsoft.jdbc.SqlServerConnection

这时候,我们作为用户,只要配合一波反射,就能在程序中动态生成Connection类辽~

public class Driver{
    public static Connection getConnection(String dbType,Properties info){
        Class<?> clz = getConnectionImplClass(dbType);
        try{
            Constructor<?> c = clz.getConstructor(Properties.class);
            return (Connection) c.newInstance(info);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    private static Class<?> getConnectionImplClass(String dbType){
        // 读取配置文件,从中根据dbType来读取相应的Connection实现类
    }
}

这样是不是优雅了很多呢?

但是!还有两个问题:

  • 客户使用时,还需要提供一个配置文件,且配置文件还要把具体实现类写对才行
  • 创建实现类的过程被暴露出来了,我们用户竟然还要自己反射,不能这样!要让各个数据库厂商在各自的jar包里创建自家的Connection实例对象

工厂方法

为了解决上述问题,我们决定用更高级的工厂方法,而非简单工厂

// 属于jdk的Driver类
public interface Driver {
    public Connection getConnection(Properties info);
}

// 属于mysql-jdbc.jar的MysqlDriver类
public class MysqlDriver implements Driver {
    public Connection getConnection(Properties info) {
        return new MysqlConnectionImpl(info); 
    }
}

// 属于oracle-jdbc.jar的OracleDriver类
public class OracleDriver implements Driver {
    public Connection getConnection(Properties info) {
        return new OracleConnectionImpl(info); 
    }
}

// ...

你有没有疑问,我这样是不是引入了新的问题,我难不成还得new出来?

Driver driver = new MysqlDriver();
Connection conn = driver.getConnection(info);

不不不,我们还是继续反射就行,让它动态创建

Class<?> clz = Class.forName("com.mysql.MysqlDriver");
Driver driver = (Driver) clz.newInstance();
Connection conn = driver.getConnection(info);

啊这,一直说反射,但我不会啊...

得,咱们继续简化!

// 驱动管理
public class DriverManager {
	// 驱动注册表
    private static List<Driver> registeredDrivers = new ArrayList<>();
    
    // 通过配置,获得连接
    public Connection getConnection(String url,String user,String pswd) {
        Properties info = new Properties();
        info.put("user",user);
        info.put("pswd",pswd);
        for(Driver driver : registeredDrivers){
            Connection conn = driver.getConnection(url,info); 
            if(conn != null) {
                return conn;
            }
        }
        throw new RuntimeException("Connection Failed!");
    }
    
    // 不存在注册表里,就注册驱动
    public static void register(Driver driver){
        if(!registeredDrivers.contains(driver)){
            registeredDrivers.add(driver);
        }
    }
}

【额外说一句,当类被加载到jvm里,静态成员变量和静态代码块会先执行】

我们再来看看Mysql的具体驱动现在要怎么写

public class MysqlDriver implements Driver {
    
    // 在Mysql驱动类被装载时,就注册到DriverManager的注册表中
    static{
        DriverManager.register(new MysqlDriver());
    }
    
    // 获取具体连接
    public Connection getConnection(String url,Properties info) {
        if(acceptsURL(url)){
            return new MysqlConnectionImpl(info); 
        }
        return null;
    } 
    
    // 格式检查
    public boolean acceptsURL(String url){
        return url.startsWith("jdbc:mysql");
    }
}

芜湖~起飞!✈️

快来看看怎么用的!

Class.forName("com.mysql.MysqlDriver");
Connection conn = DriverManager.getConnection(
				"jdbc:mysql://localhost:3306/studb",
				"root",
				"admin");

可以吧!是不是有内味了!我们来复盘一下:

  • 装载:首先把Mysql的具体驱动类MysqlDriver(class类模板)通过反射加载到jvm里

  • 注册驱动:装载后,MysqlDriver执行静态代码块,new了一个MysqlDriver的实例对象,注入DriverManager的注册方法,完成注册

  • 获取连接:根据配置信息,返回连接对象

【关于Class.forName,其实在JDBC4.0之后的规范是不需要写的,但为了兼容老版本的JDBC规范,还是写上比较好,强制加载,保证不出错】

到这里,我们就完成了数据库的连接啦!一套体系就出来辽!!!


ORM的出现

又臭又长的JDBC

看似问题解决了,但是开发者们似乎还是有很多的抱怨,我就是一个简单的select * from也要写一堆,不信你瞧:

package com.microsoft.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class JDBCDemo {
    public static void main(String[] args){
        try {
            //1.加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //2.获取连接对象
            String url = "jdbc:mysql://localhost:3306/how2java?useUnicode=true&characterEncoding=UTF-8";
            Connection connection = DriverManager.getConnection(url,"root","admin");
            //3.定义sql语句
            String sql = "select * from account";
            //4.获取执行sql语句的表单对象
            Statement statement = connection.createStatement();
            //5.执行sql
            ResultSet resultSet = statement.executeQuery(sql);
            //6.处理结果
            while(resultSet.next()){
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                Double money = resultSet.getDouble("money");
                System.out.println(id+" "+name+" "+money);
            }
            //7.释放资源
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
   }
}

啊这,好像是这么回事,那咋办嘛?


JDBC模板出现

仔细想想,其实数据库访问无外乎这几件事情:

  • 指定数据库连接参数

  • 打开数据库连接

  • 声明SQL语句

  • 预编译并执行SQL语句

  • 遍历查询结果

  • 处理每一次遍历操作

  • 处理抛出的任何异常

  • 处理事务

  • 关闭数据库连接

那我们是不是可以尝试写一个JDBC的模板?比如这样:

List<User> users = this.jdbcTemplate.query(
	"select id,name from users",
    new RowMapper<User>(){
        public User mapRow(ResultSet rs,int rowNum) throws SQLException{
            User user = new User();
            user.setID(rs.getInt("id"));
            user.setName(rs.getString("name"));
            return user;
        }
    }
);

这样一来,就使得我们更加专注于业务,而非连接的创建上!

可问题是你this.jdbcTemplate这个对象哪里来的啊?这个问题不大:

// 获取数据源(伪代码)
DataSource ds = Tool.getDataSource();
this.jdbcTemplate = new JdbcTemplate(ds);

我们只要把数据源注入JDBC模板中就行啦!

JDBC模板这样对JDBC进行封装 ,的确把数据库的访问向前推进了一大步,但是我们的本质的问题仍然没有解决!

什么本质问题?

这个问题就是面向对象世界和关系数据世界之间存在的巨大鸿沟。

就比如说,ResultSet依然是对一个表的数据的抽象和模拟:rs.next() 获取下一行,rs.getXXX() 访问该行某一列;把关系数据转化成Java对象的过程,仍然需要码农们写大量代码来完成!

这时候救星出现了——我们的主角,ORM!!!


ORM救星的到来

啥是ORM呢?别被它洋气的名字吓到了!其实就是对象关系映射(Object Relational Mapping)

  • 啥是对象?自然是指Java对象了嘛

  • 啥是关系?自然是SQL数据库的一张张表了嘛

我们约定几个原则:

  • 数据库的表映射为Java 的类(class)

  • 表中的行记录映射为一个个Java 对象

  • 表中的列映射为Java 对象的属性

都是一一对应的嗷~

但咱们说是这么说,但实际操作起来就遇到了一堆麻烦,咱随便说几个:

  • Java类的粒度要精细的多, 有时候多个类合在一起才能映射到一张表

  • SQL没有继承一说

  • 对象标识不同,Java用==或者是equals,而SQL用的是主键

  • 对象之间互相关联依赖的问题,SQL只能外键、关联表了

  • Java中数据导航容易,比如City c = user.getAddress().getCity();,但是SQL就得使用上连接

  • Java中的对象无非就是要用创建,不用回收;但是涉及到数据库,就得考虑持久化状态(是否写入磁盘)

  • 等等等等!!!!!!!!

所以我们要感谢ORM框架的开发者们啊!!!

  • 消除了 90%以上的JDBC API代码
  • SQL从Java代码中解耦出来,写到xml配置中,可复用,可读性好
  • 数据类型转换的自动化

推荐视频🔗

posted @ 2020-10-02 13:05  王帅真  阅读(444)  评论(0编辑  收藏  举报