【持久层】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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章