你真的了解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配置中,可复用,可读性好
- 数据类型转换的自动化