JDBC的执行流程

一、JDBC的层次结构

总体而言,JDBC包含以下几大角色 : Driver、DriverManager、Connection、Statement、ResultSet。这几大角色之间的层次关系如下图所示:

Connection:Driver 或者 DriverManager根据连接的url 和参数信息创建Connection实例,用来维持和数据库的数据通信,如果没有销毁或者调用close()对象,此对象和数据库的对象会一直保持连接;

Statement:Connection创建Statement对象,表示需要执行的sql语句或者存储过程;

ResultSet: 表示Statement执行完SQL语句后返回的结果集。

Connection角色

Connection表示与特定数据库的连接,可以获取到数据库的一些信息,这些信息包括:其表信息,应该支持的SQL语法,数据库内有什么存储过程,此链接功能的信息等等。

在一般实际使用情况下,我们关注的Connection的功能有以下几点:

1.创建可以执行sql语句或者存储过程的对象statement,用来和数据库进行交互;

比如,以下代码创建了几种不同类型的Statement:

			//加载Oracle数据库驱动
			Class.forName("oracle.jdbc.driver.OracleDriver");
			
			//根据特定的URL,返回可以接受此URL的数据库驱动对象
			Driver driver = DriverManager.getDriver(URL);
			
			//使用数据库驱动创建数据库连接Connection会话
			Connection connection = driver.connect(URL, props);
		    
			//创建静态的sql语句  Statement 对象来将 SQL 语句发送到数据库。
			Statement staticStatement= connection.createStatement();
 
			//创建CallableStatement 对象来调用数据库存储过程。
			CallableStatement callableStatement = connection.prepareCall(sqlString);
			
			//创建参数化的Statement对象
			PreparedStatement preparedStatement = connection.prepareStatement(sqlString);

2. 控制sql语句的事务;

Connection默认情况下,对于创建的statement执行的sql语句都是自动提交的,即在statement语句执行完后,自动执行commit操作,将结果影响到物理数据库。为了满足更好地事务控制需求,我们也可以手动地控制事务,手动地对statement 的sql语句执行进行提交(commit)或者回滚(rollback)。

下面通过一个简单的例子演示connection的事务控制:

			String sqlString="insert into tableName(column1,column2) values(value1,value2)";
			//加载Oracle数据库驱动
			Class.forName("oracle.jdbc.driver.OracleDriver");
			
			//根据特定的URL,返回可以接受此URL的数据库驱动对象
			Driver driver = DriverManager.getDriver(URL);
			
			//使用数据库驱动创建数据库连接Connection会话
		    connection = driver.connect(URL, props);
		    //使用自定义的事务,要设置connection不自动提交
		    connection.setAutoCommit(false);
		    
			//创建静态的sql语句  Statement 对象来将 SQL 语句发送到数据库。
			Statement staticStatement= connection.createStatement();
			try{
				//执行插入操作
				staticStatement.execute(sqlString);
                 // 和上面的connection等价,statement只有一个创建自身的connection的引用
				staticStatement.getConnection().commit();
			}catch(Exception e)
			{
				//有异常,则rollback
				staticStatement.getConnection().rollback();
			}

3.获取数据库连接的元数据,即数据库的整体综合信息。

具体DatabaseMetaData内包含了什么信息,请查看 JDK 的API对DatabaseMetaData的描述。

三、Statement

Statement 的功能在于根据传入的sql语句,将传入sql经过整理组合成数据库能够识别的sql语句。

(对于静态的sql语句,不需要整理组合;而对于预编译sql语句和批量语句,则需要整理),然后传递sql请求,之后会得到返回的结果。

也就是说Statement用来操作sql语句(增删改查),并返回相应结果对象。

对于查询sql,结果会以ResultSet的形式返回。

SQL分类

SQL语句可以分为增删改查(CRUD,Create,Read,Update,Delete)四种形式,JDBC 从对数据更新与否的角度上看,将上面的四种形式分为两类:查询类别和更新类别。即:

查询类别:select 语句

更新类别:Insert 、update、delete语句

执行SQL方法分类

ResultSet  executeQuery(String sql) 根据查询语句返回结果集。只能执行**select**语句。

int executeUpdate(String sql) 根据执行的DML(insert update delete)语句,返回受影响的行数。

boolean execute(String sql)  此方法可以执行任意sql语句。返回boolean值. 

	true:  执行select有查询的结果

	false:  执行insert, delete,update, 执行select没有查询的结果

对应地,Statement执行sql的几种形式:

1、对sql语句类型不进行区分,执行sql语句的方法

//执行给定的 SQL 语句,该语句可能返回多个结果。
execute(String sql)

如果是执行的sql是查询类型的select语句,此方法会返回true,需要自己再调用 statement.getResultSet() 方法来获取Resultset结果集

如果是执行的更新类的sql语句如 update,delete,insert语句,此方法会返回false,自己调用statement.getUpdateCount() 返回sql语句影响的行数

2、对查询类型的sql语句的执行方法

statement提供了executeQuery(String sql)方法支持此形式,定义如下

// 执行给定的 SQL 语句,该语句返回单个 ResultSet 对象。
executeQuery(String sql)          

3. 对更新类的sql语句 的执行方法

statement提供了executeUpdate(String sql)方法支持此形式,定义如下:

// 执行给定 SQL 语句,该语句可能为 INSERT、UPDATE 或 DELETE 语句,或者不返回任何内容的 SQL 语句(如 SQL DDL 语句)。
executeUpdate(String sql)          

4.批量sql的执行方法

有时候需要将一些sql语句一起提交给数据库,批量执行,statement提供了一些方法,对批量sql的支持:

# 将给定的 SQL 命令添加到此 Statement 对象的当前命令列表中。
addBatch(String sql)          
# 将一批命令提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组。
executeBatch()          

这里只讨论一般性的Statement,不包含其子接口PreparedStatement和CallableStatement

ParameterMetaData元数据信息

ParameterMetaData是由preparedStatement对象通过getParameterMetaData方法获取而来,ParameterMetaData可用于获取有关PreparedStatement对象和其预编译sql语句 中的一些信息. eg:参数个数,获取指定位置占位符的SQL类型

获得ParameterMetaData:

ParameterMetaData parameterMetaData =  preparedStatement.getParameterMetaData ()

ParameterMetaData相关的API

  • int getParameterCount(); 获得参数个数
  • int getParameterType(int param) 获取指定参数的SQL类型。 (注:MySQL不支持获取参数类型)
public class ParameterMetaTest {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:mysql://localhost:3306/jdbc";
        java.util.Properties info = new java.util.Properties();
        info.setProperty("user","root");
        info.setProperty("password","root");
        Connection connection = DriverManager.getConnection(url, info);
        String sql  = "select * from account where id = ? and name = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1,2);
        preparedStatement.setString(2,"ls");
        ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
        int parameterCount = parameterMetaData.getParameterCount();
        // 2
        System.out.println("获取得到预编译中的参数个数:"+parameterCount);
        String parameterClassName = parameterMetaData.getParameterClassName(1);
        // 下面的会报错
        System.out.println("获取得到参数类型:"+parameterClassName);
    }
}

控制台输出:

获取得到预编译中的参数个数:2
Exception in thread "main" java.sql.SQLException: Parameter metadata not available for the given statement

Statement分类

在使用数据库连接来创建得到对应的statement时,这个时候需要注意的是

如果是下面这种创建方式;

Statement statement = connection.createStatement();

那么就会出现SQL注入问题。

举个例子:

Connection connection = JdbcUtils.getConnection();
Statement statement = connection.createStatement();
String sql = "SELECT * FROM user WHERE username = '"+username+"' AND password = '"+password+"'";
ResultSet resultSet = statement.executeQuery(sql);
User user = null;

while (resultSet.next()){
    user  = new User(
        resultSet.getInt("id"),
        resultSet.getString("username"),
        resultSet.getString("password"),
        resultSet.getString("nickname")
    );
}

当输入的密码 ' or '' = ' , 发现永远登录成功。

所以应该来避免这种SQL注入问题。

所以有了PreparedStatement,这个statement对象的创建如下:

PreparedStatement preparedStatement = connection.prepareStatement(sql);

具体的实现是:

//b.创建预编译的SQL语句对象(SQL参数需要使用?占位)
String sql = "SELECT * FROM user WHERE username  = ? AND password = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//c.设置参数, 执行(还是executeQuery()和executeQUpdate(), 但是不需要再传入SQL语句, 上面已经传入了)
preparedStatement.setString(1,username);
preparedStatement.setString(2,password);
ResultSet resultSet = preparedStatement.executeQuery();

这样子就能够避免SQL注入问题了。现在用的都是这种方式来进行操作的。

四、ResultSet

当Statement查询sql执行后,会得到ResultSet对象,ResultSet对象是sql语句查询的结果,作为数据库结果的映射,其映射关系如下图所示。

ResultSet对从数据库返回的结果进行了封装,使用迭代器的模式逐条取出结果集中的记录。其遍历结果集的基本形式如下:

while(resultSet.next())
{
    //传入列名或者列索引获取记录中对应列的值
    resultSet.getXXX(param);
}

ResultSet游标的移动和定位

Resultset 提供了很多游标定位的方法,部分方法已经在下面列出:

# 将光标移动到此 ResultSet 对象的给定行编号。
absolute(int row)          
#  将光标移动到此 ResultSet 对象的末尾,正好位于最后一行之后。
afterLast()         
# 将光标移动到此 ResultSet 对象的开头,正好位于第一行之前。
beforeFirst()         
# 将光标移动到此 ResultSet 对象的第一行。
first()
# 获取当前行编号
getRow()
# 获取光标是否位于此 ResultSet 对象的最后一行之后
isAfterLast()
# 获取光标是否位于此 ResultSet 对象的第一行之前
isBeforeFirst()
#  获取光标是否位于此 ResultSet 对象的第一行
isFirst()
# 获取光标是否位于此 ResultSet 对象的最后一行
isLast()
# 将光标移动到此 ResultSet 对象的最后一行
last()
#  将光标从当前位置向前移一行
next()
# 将光标移动到此 ResultSet 对象的上一行
previous()
# 按相对行数(或正或负)移动光标
relative(int rows)

ResultSet结果集的元数据信息

元信息是指关于 ResultSet 对象中列的类型和属性信息的对象。

元数据: 描述数据的数据. mysql元数据: 用来定义数据库, 表 ,列信息的 eg: 参数的个数, 列的个数, 列的类型...

ResultSetMetaData是由ResultSet对象通过getMetaData方法获取而来,ResultSetMetaData可用于获取有关ResultSet对象中列的类型和属性的信息。

获得ResultSetMetaData:

ResultSetMetaData resultSetMetaData =  resultSet.getMetaData()

相关API展示:

  • getColumnCount(); 获取结果集中列项目的个数
  • getColumnName(int column); 获得数据指定列的列名
  • getColumnTypeName();获取指定列的SQL类型
  • getColumnClassName();获取指定列SQL类型对应于Java的类型

写个代码来进行演示:

public class ResultSetMetaTest {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:mysql://localhost:3306/jdbc";
        java.util.Properties info = new java.util.Properties();
        info.setProperty("user","root");
        info.setProperty("password","root");
        Connection connection = DriverManager.getConnection(url, info);
        PreparedStatement preparedStatement = connection.prepareStatement("select * from account");
        ResultSet resultSet = preparedStatement.executeQuery();
        ResultSetMetaData metaData = resultSet.getMetaData();
        System.out.println("列的个数:" + metaData.getColumnCount());
        for (int i = 0; i < metaData.getColumnCount(); i++) {
            String columnTypeName = metaData.getColumnTypeName(i + 1);
            String columnName = metaData.getColumnName(i + 1);
            int columnType = metaData.getColumnType(i + 1);
            String columnClassName = metaData.getColumnClassName(i + 1);
            System.out.println("对应的列的SQL类型是:"+columnTypeName);
            System.out.println("对应的列名是:"+columnName);
            System.out.println("列对应的sql类型:"+columnType);
            System.out.println("列对应的java类型是:"+columnClassName);
            System.out.println("---------------------------");
        }
        System.out.println("第1列的名字:"+metaData.getColumnName(1));
        System.out.println("第2列的数据库类型:"+metaData.getColumnTypeName(2));
        System.out.println("第2列的java类型:"+metaData.getColumnClassName(2));
        connection.close();
    }
}

控制台输出:

列的个数:3
对应的列的SQL类型是:INT
对应的列名是:id
列对应的sql类型:4
列对应的java类型是:java.lang.Integer
---------------------------
对应的列的SQL类型是:VARCHAR
对应的列名是:name
列对应的sql类型:12
列对应的java类型是:java.lang.String
---------------------------
对应的列的SQL类型是:DOUBLE
对应的列名是:money
列对应的sql类型:8
列对应的java类型是:java.lang.Double
---------------------------
第1列的名字:id
第2列的数据库类型:VARCHAR
第2列的java类型:java.lang.String

ResultSet.getXXX(param) 、ResultSet.updateXXX()的XXX问题

ResultSet接口常用API

  • boolean next();将光标从当前位置向下移动一行
  • int getInt(int colIndex)以int形式获取ResultSet结果集当前行指定列号值
  • int getInt(String colLabel)以int形式获取ResultSet结果集当前行指定列名值
  • float getFloat(int colIndex)以float形式获取ResultSet结果集当前行指定列号值
  • float getFloat(String colLabel)以float形式获取ResultSet结果集当前行指定列名值
  • String getString(int colIndex)以String 形式获取ResultSet结果集当前行指定列号值
  • String getString(String colLabel)以String形式获取ResultSet结果集当前行指定列名值
  • Date getDate(int columnIndex); 以Date 形式获取ResultSet结果集当前行指定列号值
  • Date getDate(String columnName);以Date形式获取ResultSet结果集当前行指定列名值
  • void close()关闭ResultSet 对象

因为在ResultSet中提供了大量的API设计,所以MySQL为了满足这种条件,也应该来提供对应的一些转换器来进行设置值。

JDBC中定义了数据库中的数据类型和java数据类型的映射,用于数据库和Java数据类型之间的转换。

在使用ResultSet去记录中的某一列值的时候,用户要根据数据库对应列的数据类型地应的java数据类型,否则的话有可能抛出异常。

下图定义了数据库和Java类型之间的映射:


五、JDBC工作的基本流程

一个基本的JDBC工作流程,分为以下几步:

1.加载特定数据库驱动器实现类,并注册驱动器(Driver会注册到DriverManager中);
  1. 根据特定的URL,返回可以接受此URL的数据库驱动对象Driver;
  2. 使用数据库驱动 Driver 创建数据库连接Connection会话;
  3. 使用 Connection对象创建 用于操作sql的Statement对象;
  4. statement对象执行 sql语句,返回结果ResultSet 对象;
  5. 处理ResultSet中的结果;
  6. 关闭连接,释放资源。

以下是一个简单的案例:

public class DBConnection {
 
	static final String  URL = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
	static final String USER_NAME ="louluan";
	static final String PASSWORD = "123456";
	
	public static void main(String[] args) {
		connectionTest();
	}
	
	public static void connectionTest(){
		
		Connection connection = null;
		Statement statement = null;
		ResultSet resultSet = null;
		
		try {
			//1.加载类,并注册驱动器(Driver会注册到DriverManager中)
			
			//加载Oracle数据库驱动
			Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
			
			//2.根据特定的URL,返回可以接受此URL的数据库驱动对象
			Driver driver = DriverManager.getDriver(URL);
			Properties props = new Properties();
			props.put("user", USER_NAME);
			props.put("password", PASSWORD);
			
			//3.使用数据库驱动创建数据库连接Connection会话
			connection = driver.connect(URL, props);
			
			//4.获得Statement对象
			statement = connection.createStatement();
			//5.执行 sql语句,返回结果
			resultSet = statement.executeQuery("select * from hr.employees");
			//6.处理结果,取出数据
			while(resultSet.next())
			{
				System.out.println(resultSet.getString(2));
			}
			
			//7.关闭链接,释放资源
		} catch (ClassNotFoundException e) {
			System.out.println("加载Oracle类失败!");
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			    //使用完成后管理链接,释放资源,释放顺序应该是: ResultSet ->Statement ->Connection
				try {
					resultSet.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
				
				try {
					statement.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
				
				try {
					connection.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
		}
	}
}

JDBC工作时序图

上述几个对象之间:DriverManager或者Driver创建Connection、Connection创建Statement、Statement又获得ResultSet,它们之间的交互序列图如下所示:

posted @ 2022-09-01 22:56  雩娄的木子  阅读(1082)  评论(0编辑  收藏  举报