【持久層】JDBC詳解之基本操作

一、概述

    本文旨在傳遞更多JDBC的細節和原理。環境是Mysql,後續會繼續介紹事務、數據源和獲取數據庫元信息。

    本文涉及問題概覽:

    1. Class.forName這句代碼做了什麼?

    2. Class.forName和ClassLoader.loadClass區別?

    3. DriverManager介紹;


二、詳解

    簡單的JDBC操作,有以下幾個步驟:

    1. 註冊驅動;

    2. 獲取鏈接;

    3. 創建執行對象;

    4. 執行SQL語句獲得結果集;

    5. 處理結果集;

    6. 關閉資源;

    一個完整的小例子,旨在引出下文。

package cn.wxy.jdbc;

import java.io.Serializable;
/**
 * domain對象,用於封裝數據
 * @author wxy51
 */
public class User implements Serializable {
	private static final long serialVersionUID = 7823619631737327735L;
	private Integer id;
	private String username;
	private String password;
	private String gender;

	public User() {
	}

	public User(Integer id, String username, String password, String gender) {
		super();
		this.id = id;
		this.username = username;
		this.password = password;
		this.gender = gender;
	}

	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 getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

	public String toString() {
		return "User [id=" + id + ", username=" + username + ", password="
				+ password + ", gender=" + gender + "]";
	}
}
package cn.wxy.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class SimpleJdbcOperation {
	/**
	 * jdbc查詢簡單示例
	 * @param id 用戶id
	 * @return 數據庫中該用戶id對應記錄
	 */
	public User queryOneUser(Integer id) {
		if(id == null)
			return null;
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rSet = null;
		User user = null;
		try {
			Class.forName("com.mysql.jdbc.Driver");
			conn = DriverManager
					.getConnection("jdbc:mysql://localhost:3306/demo", "用戶名",
							"密碼");
			pstmt = conn.prepareStatement("select * from user where id=?");
			pstmt.setInt(1, id);
			rSet = pstmt.executeQuery();
			if (rSet.next())
				user = new User(rSet.getInt(1), rSet.getString(2),
						rSet.getString(3), rSet.getString(4));
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (conn != null)
				try {
					conn.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			if (pstmt != null)
				try {
					pstmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			if (rSet != null)
				try {
					rSet.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}

			conn = null;
			pstmt = null;
			rSet = null;
		}
		return user;
	}
}

1. 註冊驅動

    在JDK API中,註冊驅動的方式(具體在java.sql.Driver接口的API中)已經明確指出“When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a driver by calling Class.forName(your driver path)”。

    以mysql環境爲例,推薦的驅動註冊方式爲Class.forName("com.mysql.jdbc.Driver");,參數“com.mysql.jdbc.Driver”是mysql jdbc驅動包中實現了java.sql.Driver接口規範的驅動實現類,先不看com.mysql.jdbc.Driver的源碼,Class.forName這句代碼做了什麼?

@CallerSensitive
public static Class<?> forName(String className)
			throws ClassNotFoundException {
	Class<?> caller = Reflection.getCallerClass();
	return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader,
					Class<?> caller) throws ClassNotFoundException;
    查看Class的源碼,這句話是根據一個類的全量路徑獲得這個類的Class對象,但Class.forName這句代碼所做的事情絕不僅僅只是獲取Class對象這麼一點。

    類加載器是JDK中一個模塊,這個模塊描述的是將一個類的字節碼文件加載到內存,併爲之生成java.lang.Class對象作爲其對外訪問的接口,他涉及三個環節五個步驟,加載、連接(驗證、準備、解析)和初始化,java的初始化知識涉及類初始化和對象初始化,那麼在類加載過程中,準備和初始化兩個步驟則描述了類初始化的過程中static修飾的變量和代碼塊的默認初始化和語句執行,其實也就是對應着生成類構造器<clinit>和執行類構造器的過程,從代碼層面上來講,就是static修飾的屬性賦值語句和靜態代碼塊中的語句將會被執行。

    和類加載器ClassLoader的loadClass相同的是,這兩個方法都是根據一個類的全量路徑加載這個類的字節碼並生成java.lang.Class對象返回,但是loadClass只涉及了五個步驟中的第一個“加載”,而Class.forName則完整的經歷了五個步驟(可嘗試用兩種方式去加載一個含有靜態代碼塊的類,你會發現loadCalss的加載方式將不會執行靜態代碼塊中的內容),也就意味着loadClass不會使得類中的靜態屬性或靜態代碼塊的語句被執行。

    鋪墊夠了,那麼回過頭來看com.mysql.jdbc.Driver的源碼,除了一個空構造方法之外,就只有一個靜態代碼塊,代碼塊的內容就是註冊驅動及其過程。而根據JDBC的規範,代碼Class.forName("com.mysql.jdbc.Driver")語義將會使得這個靜態代碼塊被執行。

package com.mysql.jdbc;

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
	static {
		try {
			java.sql.DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
			throw new RuntimeException("Can't register driver!");
		}
	}

	public Driver() throws SQLException {
		// Required for Class.forName().newInstance()
	}
}

    因此可以得出結論:

    1. Class.forName這句代碼通過java類加載的知識可知其實現了驅動註冊,而根據JDBC規範可知,數據庫廠商如果支持JDBC技術,那麼無論是mysql還是oracle等等廠商,都可以通過Class.forName來實現驅動的註冊;因此可以預見的是,實現JDBC的驅動包中,幾乎都會有這樣的代碼出現;

    2. 不應該通過DriverManager.registerDriver(new com.mysql.jdbc.Driver())這樣的代碼來實現驅動註冊,通過對象實例化的知識可知這樣會導致驅動重複註冊;

    3. 是否會導致同一個數據庫被重複註冊多個驅動?答案是不會的,因爲static代碼塊只會被執行一次,如果要實現連接池一類的引用,那麼應該顯式的調用DriverManager.registerDriver去註冊多個驅動;

2. DriverManager

    java.sql.DriverManager是JDBC中提供的少有的不是接口或抽象類的類,它裏面全是靜態方法,是JDBC規範中對外提供的一個工具類,用來註冊驅動以及管理成功註冊的驅動,還有就是從驅動獲取數據庫連接。驅動源碼下一小結在看,先看看註冊驅動的過程。

private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException {
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
        println("DriverManager.getConnection(\"" + url + "\")");

        SQLException reason = null;
        for(DriverInfo aDriver : registeredDrivers) {
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
}
    既然是通過驅動來獲取連接對象,那麼首先要清楚的問題是如何從ArrayList中獲取正確的驅動對象。源碼已經透露很多信息了,通過isDriverAllowed方法來判定該驅動是否適配改數據庫,其實就是遍歷存放已註冊驅動的ArrayList,return第一個適配的驅動,通過比較該結構中的每一個驅動對象的Class和類加載器對象是否相同,如果相同則表示該驅動適配改數據庫,接着就進行獲取鏈接。

3. 獲得連接

    未完待續






三、補充

    持久層技術,JDBC只是開篇,後續進階apache commons DButils輕量級JDBC框架,再進一步spring JDBC,屆時將接觸spring獨家的事務的傳播特性,最後是Mybatis,不完全ORM的持久層框架,最後面是逐漸淪爲雞肋的完全ORM框架hibernate,至於hibernate,有時間就看吧,依然雞肋。

    但只看JDBC,除了基本操作之外,還有事務和數據源已經獲取數據庫元信息,本文只介紹了基本操作,後續還會繼續講解其他部分。

發佈了82 篇原創文章 · 獲贊 95 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章