首先薪寓,帶上先帶上問題進(jìn)行思考亡资,既然JDBC已經(jīng)能完成與數(shù)據(jù)庫的交互,已經(jīng)能夠完成對數(shù)據(jù)庫的CRUD操作向叉,為什么后期還會出現(xiàn)Mybatis呢锥腻?
是不是意味著JDBC在與數(shù)據(jù)庫進(jìn)行交互時,本身還是存在一些問題母谎,正是因?yàn)榇嬖趩栴}瘦黑,所以后期出現(xiàn)了很多持久層框架,將JDBC對數(shù)據(jù)庫的操作進(jìn)行了封裝奇唤,在封裝過程中幸斥,對JDBC存在的問題進(jìn)行規(guī)避和解決,而Mybatis只是眾多持久層框架其中之一
接下來新建一個簡單的maven項(xiàng)目咬扇,對JDBC代碼進(jìn)行一個簡單的回顧甲葬,并分析JDBC在與數(shù)據(jù)庫進(jìn)行交互時究竟存在哪些問題,然后給出對應(yīng)問題的解決思路懈贺,并在此基礎(chǔ)上完成一個自定義持久層框架(為什么需要完成這樣一個自定義這樣一個持久層框架呢经窖,其實(shí)此框架就是Mybatis的一個雛形,對后期翻閱Mybatis源碼會大有幫助)
一梭灿、JDBC代碼回顧及問題分析
新建一個maven項(xiàng)目画侣,項(xiàng)目中的pom.xml文件內(nèi)容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>study.lagou.com</groupId>
<artifactId>jdbc</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
</project>
User實(shí)體對象
package study.lagou.com.jdbc.pojo;
/**
* @Description: 功能描述
* @Author houjh
* @Email: happyxiaohou@gmail.com
* @Date: 2021-1-26 0:18
*/
public class User {
/**
* 主鍵信息
*/
private Integer id;
/**
* 用戶名稱
*/
private String username;
/**
* 用戶密碼
*/
private String password;
/**
* 用戶昵稱
*/
private String nickname;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
JDBC連接數(shù)據(jù)操作類
package study.lagou.com.jdbc;
import study.lagou.com.jdbc.pojo.User;
import java.sql.*;
/**
* @Description: 功能描述
* @Author houjh
* @Email: happyxiaohou@gmail.com
* @Date: 2021-1-25 23:32
*/
public class JDBCTest {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.cj.jdbc.Driver");
//通過驅(qū)動管理類獲取到一個connection數(shù)據(jù)庫連接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC",
"root","111111");
//編寫對應(yīng)的SQL語句,其中?表示參數(shù)的占位符
String sql = "select * from user where username = ?";
//通過connection和sql獲取到數(shù)據(jù)庫預(yù)處理對象PreparedStatement
preparedStatement = connection.prepareStatement(sql);
//借助數(shù)據(jù)庫預(yù)處理對象設(shè)置參數(shù)堡妒,第一個參數(shù)為SQL語句中參數(shù)的序號(從1開始)配乱,第二個參數(shù)為設(shè)置的參數(shù)值
preparedStatement.setString(1,"zhangsan");
//向數(shù)據(jù)庫發(fā)出SQL執(zhí)行查詢,查詢出結(jié)果集
resultSet = preparedStatement.executeQuery();
//遍歷查詢的結(jié)果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
// 封裝User
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
user.setNickname(nickname);
System.out.println(user);
}
} catch (Exception e){
e.printStackTrace();
} finally {
//釋放資源
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(preparedStatement != null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
JDBC 代碼基本回顧完成,接下來我們對代碼進(jìn)行問題分析
//加載數(shù)據(jù)庫驅(qū)動
Class.forName("com.mysql.cj.jdbc.Driver");
//通過驅(qū)動管理類獲取到一個connection數(shù)據(jù)庫連接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC","root","111111");
首先分析此段代碼搬泥,我們將數(shù)據(jù)庫的連接驅(qū)動和數(shù)據(jù)庫連接信息編寫在了JAVA代碼當(dāng)中桑寨,存在硬編碼問題,如果后期連接的數(shù)據(jù)庫發(fā)生改變佑钾,我們將要在JAVA源代碼中修改數(shù)據(jù)庫連接驅(qū)動和數(shù)據(jù)庫連接配置信息西疤,修改完成后我們必須重新編譯JAVA源文件,并再次打包部署休溶,整個過程非常麻煩
其次,這句代碼如果是在實(shí)際項(xiàng)目中使用扰她,我們需要將此段代碼寫到持久層方法當(dāng)中兽掰,如果該持久層框架被請求多次,則意味著這段代碼也會被執(zhí)行多次徒役,而每一次執(zhí)行我們都獲取到一個新的數(shù)據(jù)庫連接孽尽,然后去執(zhí)行SQL,最終再釋放這個連接忧勿,如此反復(fù)杉女,每一次請求都開啟一個新的連接,造成了數(shù)據(jù)庫連接資源的浪費(fèi)(數(shù)據(jù)庫連接可以算做是一個非常寶貴的資源鸳吸,在我們獲取數(shù)據(jù)庫連接的時候熏挎,底層需要先去建立TCP連接,完成三次握手晌砾,整個過程比較耗費(fèi)資源坎拐,影響性能)
針對以上代碼,我們總結(jié)出兩個問題
1养匈、數(shù)據(jù)庫配置信息存在硬編碼問題
2哼勇、頻繁創(chuàng)建和釋放數(shù)據(jù)庫連接
//編寫對應(yīng)的SQL語句,其中?表示參數(shù)的點(diǎn)位符
String sql = "select * from user where username = ?";
//通過connection獲取到數(shù)據(jù)庫預(yù)處理對象PreparedStatement
preparedStatement = connection.prepareStatement(sql);
//借助數(shù)據(jù)庫預(yù)處理對象設(shè)置參數(shù)呕乎,第一個參數(shù)為SQL語句中參數(shù)的序號(從1開始)积担,第二個參數(shù)為設(shè)置的參數(shù)值
preparedStatement.setString(1,"zhangsan");
//向數(shù)據(jù)庫發(fā)出SQL執(zhí)行查詢,查詢出結(jié)果集
resultSet = preparedStatement.executeQuery();
//遍歷查詢的結(jié)果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
// 封裝User
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
user.setNickname(nickname);
System.out.println(user);
}
再分析上一段代碼猬仁,所存在的問題是
3帝璧、SQL語句、設(shè)置參數(shù)逐虚、獲取結(jié)果集參數(shù)均存在硬編碼問題
//遍歷查詢的結(jié)果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
// 封裝User
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
user.setNickname(nickname);
System.out.println(user);
}
最后看封裝返回結(jié)果集對象這段聋溜,現(xiàn)在看到的是User對象屬性值比較少的情況,但是如果試想叭爱,如果User對象存在幾十撮躁、上百個屬性值的時候,則在此處設(shè)置對象屬性值的過程將變得相當(dāng)繁瑣买雾,所以此段代碼我們分析出的問題就是
4把曼、需要手動封裝返回結(jié)果集杨帽,較為繁瑣
通過對JDBC連接代碼塊進(jìn)行分析,JDBC連接操作數(shù)據(jù)庫時主要存在以下問題:
1嗤军、 頻繁創(chuàng)建注盈、釋放數(shù)據(jù)庫連接,造成系統(tǒng)資源浪費(fèi)叙赚,從?影響系統(tǒng)性能老客。
2、 sql語句在代碼中硬編碼震叮,造成代碼不易維護(hù)胧砰,實(shí)際應(yīng)?中sql變化的可能較大,sql變動需要改變java代碼苇瓣。
3尉间、 使?preparedStatement向占位符號傳參數(shù)存在硬編碼,因?yàn)閟ql語句的where條件不?定击罪,可能多也可能少哲嘲,修改sql還要修改代碼,系統(tǒng)不易維護(hù)媳禁。
4眠副、 對結(jié)果集解析存在硬編碼(查詢列名),sql變化導(dǎo)致解析代碼變化损话,系統(tǒng)不易維護(hù)侦啸,如果能將數(shù)據(jù)庫記錄封裝成pojo對象解析比較方便
二、針對分析出的問題丧枪,給出問題解決思路
1光涂、數(shù)據(jù)庫配置信息存在硬編碼問題
看到硬編碼,我們?nèi)菀茁?lián)想到配置文件拧烦,此處給出的解決方案也是通過配置文件解決硬編碼問題
2忘闻、頻繁創(chuàng)建和釋放數(shù)據(jù)庫連接
此處問題我們可以通過數(shù)據(jù)庫連接池來進(jìn)行解決
3、SQL語句恋博、設(shè)置參數(shù)齐佳、獲取結(jié)果集參數(shù)均存在硬編碼問題
同樣通過配置文件來處理,但是此處配置文件建議和問題1中的配置文件分開來處理债沮,因?yàn)閱栴}1中的配置文件炼吴,保存的是數(shù)據(jù)庫連接的基本信息,相對來說內(nèi)容比較固定疫衩,不容易發(fā)生改變硅蹦,而問題3中所使用的配置文件,主要是用來存儲SQL語句、參數(shù)童芹、返回結(jié)果集等信息涮瞻,比較容易發(fā)生改變,所以建議和問題1中的配置文件分開
4假褪、需要手動封裝返回結(jié)果集署咽,較為繁瑣
針對這個問題,我們可以通過使用反射生音、內(nèi)省等技術(shù)去完成查詢出來的結(jié)果集與實(shí)體中屬性的自動轉(zhuǎn)換封裝
三宁否、自定義框架設(shè)計
前面進(jìn)行了JDBC代碼回顧以及問題分析,并給出了解決問題的基本思路缀遍,接下來我們設(shè)置一個自定義的持久層框架(注意:當(dāng)前我們自定義的持久層框架家淤,它的本質(zhì)是對JDBC代碼的一個封裝,只不過在封裝的過程當(dāng)中瑟由,我們需要對JDBC代碼存在的問題進(jìn)行規(guī)避或解決)
1、自定義持久層框架的設(shè)計思路
自定義的框架分為使用端和自定義持久層框架本身兩部分
1.1冤寿、使用端
使用端這個項(xiàng)目當(dāng)中我們將會使用自定義持久層框架來完成對數(shù)據(jù)庫的交互(對數(shù)據(jù)庫表的CRUD操作)歹苦,在使用端需要對自定義持久層框架進(jìn)行調(diào)用,那么使用端項(xiàng)目需要引入自定義持久層框架的jar包(自定義持久層框架也是一個項(xiàng)目督怜,使用端需要調(diào)用自定義持久層框架中的方法殴瘦,則需要引入對應(yīng)的jar包)
一段JDBC代碼,想要正常執(zhí)行号杠,那么必不可少的是兩部分信息蚪腋,一個是數(shù)據(jù)連接配置信息,一個是SQL配置信息姨蟋,所以使用端需要提供這兩部分配置信息屉凯,其中SQL配置信息包括SQL語句、參數(shù)類型以及返回值類型眼溶,因?yàn)槲覀円獙DBC硬編碼的問題進(jìn)行解決悠砚,所以我們通過配置文件來提供這兩部分信息
(1)、sqlMapConfig.xml:存放數(shù)據(jù)庫配置信息堂飞,存放mapper.xml的全路徑
(2)灌旧、mapper.xml:存放SQL配置信息
1.2、持久層框架本身
持久層框架本身也是一個項(xiàng)目绰筛,項(xiàng)目主要實(shí)現(xiàn)的功能就是對JDBC代碼進(jìn)行封裝枢泰,這就意味著當(dāng)使用端對自定義持久層框架進(jìn)行調(diào)用時,底層執(zhí)行的還是JDBC代碼铝噩,因?yàn)榕渲梦募嫒朐谑褂枚说呐渲梦募?dāng)中衡蚂,所以自定義持久層框架需要做的事情就是通過路徑讀取到對應(yīng)的配置文件,讀取到對應(yīng)配置文件的內(nèi)容,那么就可以調(diào)用底層的JDBC代碼進(jìn)行對數(shù)據(jù)庫的操作了(這里就是整個自定義持久層框架的一個基本思路)
有了自定義持久層框架的基本思路讳窟,那么接下來我們對具體的操作進(jìn)行細(xì)化
(1)让歼、加載配置文件(根據(jù)配置文件的路徑,將sqlMapConfig.xml和mapper.xml配置文件加載成字節(jié)輸入流丽啡,以流的形式存放在內(nèi)存當(dāng)中)
具體的做法就是創(chuàng)建一個Resources類谋右,在這個類中有一個方法,InputStream in = getResourceAsStream(String path)补箍,并且這個方法參見要傳遞一個參數(shù)path改执,這個path就是sqlMapConfig.xml和mapper.xml配置文件所存放的路徑(這里引發(fā)一個問題思考,我們在使用端有兩個配置文件坑雅,那么自定義持久層框架也要對配置文件加載2次呢辈挂?答案是可以加載2次,但是不建議加載2次裹粤,我們可以在sqlMapConfig.xml文件中引入mapper.xml文件的全路徑终蒂,在讀取sqlMapConfig.xml這個配置文件的時候,就可以將mapper.xml文件的全路徑也讀取出來遥诉,拿到文件路徑了拇泣,就方便對文件進(jìn)行操作了)
(2)、創(chuàng)建兩個javaBean(由于我們從配置文件加載出來的配置文件矮锈,是以流的形式存放到內(nèi)存當(dāng)中的霉翔,不方便進(jìn)行操作,所以我們需要創(chuàng)建兩個容器對象苞笨,將解析出來的配置文件的內(nèi)容存放到這兩個容器對象中债朵,方便操作)
Configuration:核心配置類:存放sqlMapConfig.xml解析出來的內(nèi)容
MappedStatement:映射配置類:存放mapper.xml解析出來的內(nèi)容
(3)、解析配置文件(使用dom4j來對配置文件進(jìn)行解析)
創(chuàng)建一個SqlSessionFactoryBuilder類瀑凝,該類中有一個build(Inputstream in)方法序芦,在build方法中主要完成兩件事
第一:使用dom4j解析配置文件,將解析出來的內(nèi)容封裝到容器對象中
第二:創(chuàng)建SqlSessionFactory工廠對象猜丹,主要用來生產(chǎn)sqlSession會話對象芝加,此處理用到工廠設(shè)計模式(工廠設(shè)計模式可以降低代碼間耦合度,并根據(jù)需求生產(chǎn)出不同狀態(tài)類型的對象)
(4)射窒、創(chuàng)建SqlSessionFactory接口及實(shí)現(xiàn)類DefaultSqlSessionFactory
接口中定義openSession()方法藏杖,該方法的主要作用是生產(chǎn)sqlSession
(5)、創(chuàng)建SqlSession接口及實(shí)現(xiàn)類DefaultSqlSession
定義對數(shù)據(jù)庫的CRUD操作脉顿,定義selectList()蝌麸、selectOne()、update()艾疟、delete()等方法来吩,在這些方法的實(shí)現(xiàn)當(dāng)中可以直接通過操作JDBC代碼來實(shí)現(xiàn)對數(shù)據(jù)庫的操作敢辩,但是由于存在大量的初始化JDBC配置的重復(fù)操作,所以衍生出后續(xù)一步的優(yōu)化
(6)弟疆、創(chuàng)建Executor接口及實(shí)現(xiàn)類SimpleExecutor實(shí)現(xiàn)類
定義一個query(Configuration configuration戚长,MappedStatement mappedStatement,Object... params)方法怠苔,該方法執(zhí)行的就是JDBC代碼同廉,由于執(zhí)行JDBC代碼需要對應(yīng)的配置文件,所以將兩個配置bean傳入進(jìn)來柑司,最后一個參數(shù)表示查詢時需要拼接的參數(shù)迫肖,由于不知道具體的參數(shù)個數(shù),所以直采用可變參數(shù)的寫法來處理