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中);
- 根据特定的URL,返回可以接受此URL的数据库驱动对象Driver;
- 使用数据库驱动 Driver 创建数据库连接Connection会话;
- 使用 Connection对象创建 用于操作sql的Statement对象;
- statement对象执行 sql语句,返回结果ResultSet 对象;
- 处理ResultSet中的结果;
- 关闭连接,释放资源。
以下是一个简单的案例:
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,它们之间的交互序列图如下所示: