JDBC的典型用法:
JDBC4.2常用接口和类简介:
DriverManager:用于管理JDBC驱动的服务类,程序中使用该类的主要功能是获取Connection对象,该类包含如下方法:
public static synchronized Connection getConnection(String url, String user, String pass) throws SQLException:该方法获得url对应数据库的连接
Connection:代表数据库连接对象,每个Connection代表一个物理连接会话。想要访问数据库,必须先获得数据库连接,该接口的常用方法如下:
1.Statement createStatement() throws SQLException:该方法返回一个Statement对象。
2.PreparedStatement prepareStatement(String sql) throws SQLException:该方法返回预编译的Statement对象,即将SQL语句提交到数据库进行预编译。
3.CallableStatement prepareCall(String sql) throws SQLException:该方法返回CallableStatement对象,该对象用于调用存储过程
上面三个方法都返回用于执行SQL语句的Statement对象,PreparedStatement、CallableStatement是Statement的子类,只有获得了Statement之后才可执行SQL语句。
除此之外,Connection还有如下几个用于控制事务的方法:
1.Savepoint setSavepoint():创建一个保存点
2.Savepoint setSavepoint(String name):以指定名字创建一个保存点
3.void setTransactionIsolation(int level):设置事务的隔离级别
4.void rollback():回滚事务
5.void rollback(Savepoint savepoint):将事务回滚到指定的保存点
6.void setAutoCommit(boolean autoCommit):关闭自动提交、打开事务
7.void commit():提交事务
Java7 为Connection新增了
setSchema(String schema)、getSchema()两个方法:用于控制该Connection访问的数据库Schema
setNetworkTimeout(Executor executor, int milliseconds)、getNetworkTimeout()两个方法:用于控制数据库连接超时行为。
Statement:用于执行SQL语句的工具接口,常用方法如下:
1.ResultSet executeQuery(String sql) throws SQLException:该方法用于执行查询语句,并返回查询结果对应的ResultSet对象。该方法只能用于执行查询语句
2.int executeUpdate(String sql) throws SQLException:该方法用于执行DML(数据操作语言)语句,并返回受影响的行数;该方法也可用于执行DDL(数据定义
语言)语句执行DDL语句将返回0
3.boolean execute(String sql) throws SQLException:该方法可执行任何SQL语句。若执行后第一个结果为ResultSet对象,则返回true;若执行后第一个结果为受影
响的行数或没有任何结果,则返回false。
Java7为Statement新增了closeOnCompletion()方法:若Statement执行了该方法,则当所有依赖于该Statement的ResultSet关闭时,该Statement会自动关闭。
Java7还为Statement提供了isCloseOnCompletion()方法:用于判断该Statement是否打开了“closeOnCompletion”.
Java8为Statement新增了多个重载的executeLargeUpdate()方法:这些方法相当于增强版的executeUpdate()方法,返回值类型为long,即当DML语句影响的记录条数超过
Integer.MAX_VALUE时,就应该使用executeLargeUpdate()方法。
PreparedStatement:预编译的Statement对象。PreparedStatement是Statement的子接口,它允许数据库预编译SQL语句(这些SQL语句通常带有参数),以后每次只
改变SQL命令的参数,避免数据库每次都需要编译SQL语句,因此性能更好。相对于Statement而言,使用PreparedStatement执行SQL语句时,无需再传入SQL语句,只
要为预编译的SQL语句传入参数数值即可。所以它比Statement多了如下方法:
1.void setXxx(int parameterIndex, Xxx value):该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给SQL语句中指定位置的参数。
ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针:
1.void close():释放ResultSet对象。
2.boolean absolute(int row):将结果集的记录指针移动到第row行,若row是负数,则移动到倒数第row行。若移动后的记录指针指向一条有效记录,则该方法返回true
3.void beforeFirst():将ResultSet的记录指针定位到首行之前,这是ResultSet结果集记录指针的初始状态——记录指针的起始位置位于第一行之前
4.boolean first():将ResultSet的记录指针定位到首行。若移动后的记录指针指向一条有效记录,则该方法返回true。
5.boolean previous():将ResultSet的记录指针定位到上一行。若移动后的记录指针指向一条有效记录,则该方法返回true。
6.boolean next():将ResultSet的记录指针定位到下一行,若移动后的记录指针指向一条有效记录,则该方法返回true。
7.boolean last():将ResultSet的记录指针定位到最后一行,若移动后的记录指针指向一条有效记录,则该方法返回true。
8.void afterLast():将ResultSet的记录指针定位到最后一行之后。
JDBC编程步骤:
1.加载数据库驱动:
通常使用Class类的forName()静态方法来加载驱动:
Class.forName(driverClass);//driverClass就是数据库驱动类所对应的字符串。如:加载MySQL的驱动代码
Class.forName("com.mysql.jdbc.Driver");
2.通过DriverManager获取数据库连接:
//获取数据库连接
DriverManager.getConnection(String url, String user, String pass);//数据库URL、登录数据库的用户名和密码。
数据库URL通常遵循如下写法:
jdbc:subprotocol:other stuff
jdbc是固定的写法,subprotocol指定连接到特定数据库的驱动,other stuff也不是固定的(由数据库定)
MySQL数据库的URL写法如下:
jdbc:mysql://hostname:port/databasename
3.通过Connection对象创建Statement对象。有如下三个方法:
1.createStatement():创建基本的Statement对象
2.prepareStatement(String sql):根据传入的SQL语句创建预编译的Statement对象
3.prepateCall(String sql):根据传入的SQL语句创建CllableStatement对象
4.使用Statement执行SQL语句。有如下三个方法:
1.execute():可执行任何SQL语句,但比较麻烦
2.executeUpdate():主要用于执行DML和DDL语句。执行DML语句返回受SQL语句影响的行数,执行DDL语句返回0
3.executeQuery():只能执行查询语句,执行后返回代表查询结果的ResultSet对象
5.操作结果集:
若执行SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序可通过操作该ResultSet对象来取出查询结果:
1.next()、previous()、first()、last()、beforeFirst()、afterLast()、absolute()等移动记录指针的方法
2.getXxx()方法获取记录指针指向行、特定列的值。该方法既可使用列索引作为参数,也可使用列名作为参数。使用列索引作为参数性能更好,使用列名作为参数可
读性更好
6.回收数据库资源:
包括关闭ResultSet、Statement和Connection等资源。
下面程序简单示范了JDBC编程,并通过ResultSet获得结果集的过程:
drop database if exists select_test;
create database select_test;
use select_test;
# 为了保证从表参照的主表存在,通常应该先建主表。
create table teacher_table
(
# auto_increment:实际上代表所有数据库的自动编号策略,通常用作数据表的逻辑主键。
teacher_id int auto_increment,
teacher_name varchar(255),
primary key(teacher_id)
);
create table student_table
(
# 为本表建立主键约束
student_id int auto_increment primary key,
student_name varchar(255),
# 指定java_teacher参照到teacher_table的teacher_id列
java_teacher int,
foreign key(java_teacher) references teacher_table(teacher_id)
);
insert into teacher_table
values
(null , 'Yeeku');
insert into teacher_table
values
(null , 'Leegang');
insert into teacher_table
values
(null , 'Martine');
insert into student_table
values
(null , '张三' , 1);
insert into student_table
values
(null , '张三' , 1);
insert into student_table
values
(null , '李四' , 1);
insert into student_table
values
(null , '王五' , 2);
insert into student_table
values
(null , '_王五' , 2); insert into student_table
values
(null , null , 2);
insert into student_table
values
(null , '赵六' , null);
数据库建表语句
将驱动(mysql-connector-java-5.1.30-bin.jar)放到java目录的下的jre/lib/ext/目录下面。或者将驱动的路径添加到classpath环境变量后面。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.ResultSet; public class ConnMySql{
public static void main(String[] args) throws Exception{
//1.加载驱动,使用反射知识,现在记住这么写
Class.forName("com.mysql.jdbc.Driver");
try(
//2.使用DriverManager获取数据库连接
//其中返回的Connection就代表了Java程序和数据库的连接
//不同数据库的URL写法需要查询驱动文档,用户名、密码由DBA分配
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/select_test", "root", "123456");
//3.使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement();
//4.执行SQL语句
/*
Statement有三种执行SQL语句的方法:
1.execute()可执行任何SQL语句-返回一个boolean值,若执行后第一个结果是ResultSet,则返回true,否则返回false
2.executeQuery()执行select语句-返回查询到的结果集
3.executeUpdate()用于执行DML语句-返回一个整数,代表被SQL语句影响的记录条数
*/
ResultSet rs = stmt.executeQuery("select s.*, teacher_name"
+ " from student_table s , teacher_table t"
+ " where t.teacher_id = s.java_teacher")){
//ResultSet有一系列的getXxx(列索引 | 列名)方法,用于获取记录指针
//指向行、特定列的值,不断地使用next()将记录指针下移一行
//若移动之后记录指针依然指向有效行,则next()方法返回true
while(rs.next()){
System.out.println(rs.getInt(1) + "\t"
+ rs.getString(2) + "\t"
+ rs.getString(3) + "\t"
+ rs.getString(4));
}
}
}
}
执行SQL语句的方式:
使用Java8新增的executeLargeUpdate()方法执行DDL和DML语句:
使用statement执行DDL和DML语句的步骤与执行普通查询语句的步骤基本相似,区别在于执行了DDL语句后返回值为0,执行了DML语句后返回值为受到影响的记录条数。
MySQL暂不支持executeLargeUpdate()方法。所以我们使用executeUpdate()方法。
下面的程序并没有直接把数据库连接信息写在代码中,而是使用一个mysql.ini文件(就是properties文件)来保存数据库连接信息,这是比较成熟的做法——当需要把应用程
序从开发环境移植到生产环境时,无需修改源代码,只需要修改mysql.ini配置文件即可:
mysql.ini文件内容:
mysql.ini和ExecuteDDL.java文件所放位置:
import java.io.FileInputStream;
import java.util.Properties;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement; public class ExecuteDDL{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile) throws Exception{
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public void createTable(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement()){
//执行DDL语句,创建数据表
stmt.executeUpdate(sql);
}
} public static void main(String[] args) throws Exception{
ExecuteDDL ed = new ExecuteDDL();
ed.initParam("mysql.ini");
ed.createTable("create table jdbc_test "
+ "( jdbc_id int auto_increment primary key, "
+ "jdbc_name varchar(255), "
+ "jdbc_desc text);");
System.out.println("------建表成功------");
}
}
使用executeUpdate()方法执行DML语句:
和上面程序的步骤是一样的,只不过程序代码需要修改:
import java.io.FileInputStream;
import java.util.Properties;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement; public class ExecuteDML{
private String driver;
private String url;
private String user;
private String pass; public void initParam(String paramFile) throws Exception{
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public int insertData(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement()){
//执行SQL语句,返回受影响的记录条数
return stmt.executeUpdate(sql);
}
} public static void main(String[] args) throws Exception{
ExecuteDML ed = new ExecuteDML();
ed.initParam("mysql.ini");
int result = ed.insertData("insert into jdbc_test(jdbc_name,jdbc_desc)"
+ "select s.student_name , t.teacher_name "
+ "from student_table s , teacher_table t "
+ "where s.java_teacher = t.teacher_id;");
System.out.println("------系统中一共有" + result + "条记录受影响------");
}
}
使用execute()方法执行SQL语句:
Statement的execute()方法几乎可以执行任何SQL语句,但它执行SQL语句比较麻烦,通常没有必要使用execute()方法来执行SQL语句。
使用execute()方法执行SQL语句的返回值只是boolean值,它表明执行该SQL语句是否返回了ResultSet对象,Statement提供了如下两个方法来获取执行结果:
1.getResultSet():获取该Statement执行查询语句所返回的ResultSet对象
2.getUpdateCount():获取该Statement执行DML语句所影响的记录行数。
下面程序示范了使用Statement的execute()方法来执行任意的SQL语句:
import java.util.Properties;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData; public class ExecuteSQL{
private String driver;
private String url;
private String user;
private String pass; public void initParam(String paramFile) throws Exception{
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public void executeSql(String sql) throws Exception{
//加载数据库驱动
Class.forName(driver);
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//通过Connection创建一个Statement
Statement stmt = conn.createStatement()){
//执行SQL语句,返回boolean值表示是否包含ResultSet
boolean hasResultSet = stmt.execute(sql);
//若执行后有ResultSet结果集
if(hasResultSet){
try(
//获取结果集
ResultSet rs = stmt.getResultSet()){
//ResultSetMetaData是用于分析结果集的元数据接口
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
//迭代输出ResultSet对象
while(rs.next()){
//依次输出每列的值
for(int i = 0; i < columnCount; i++){
System.out.print(rs.getString(i + 1) + "\t");
}
System.out.print("\n");
}
}
}else{
System.out.println("该SQL语句影响的记录有" + stmt.getUpdateCount() + "条");
}
}
} public static void main(String[] args) throws Exception{
ExecuteSQL es = new ExecuteSQL();
es.initParam("mysql.ini");
System.out.println("------执行删除表的DDL语句------");
es.executeSql("drop table if exists my_test");
System.out.print("------执行建表的DDL语句------");
es.executeSql("create table my_test"
+ "(test_id int auto_increment primary key, "
+ "test_name varchar(255))");
System.out.println("------执行插入数据的DML语句------");
es.executeSql("insert into my_test(test_name) "
+ "select student_name from student_table");
System.out.println("------执行查询数据的查询语句------");
es.executeSql("select * from my_test");
}
}
从结果看,执行DDL语句显示受影响记录条数;执行DML显示插入、修改、删除的记录条数;执行查询语句可以输出查询结果。
上面程序获得的SQL执行结果是没有根据各列的数据类型调用相应的getXxx()方法,而是直接使用getString()方法来取得值,这是可以的。
ResultSet的getString()方法几乎可以获取除了Blob之外的任意类型列的值,因为所有的数据类型都可以自动转换成字符串类型。
使用PreparedStatement执行SQL语句:
若经常要反复执行一条结构相似的SQL语句,如下两条:
insert into student_table values(null, '张三', 1);
insert into student_table values(null, '李四', 2);
对于这两条语句,它们结构相似,只是执行插入时插入的值不同而已。对于这种情况,可以使用占位符(?)参数的SQL语句来代替它:
insert into student_table values(null, ?, ?);
JDBC提供了PreparedStatement接口,它是Statement的子接口。它可以进行预编译SQL语句,预编译后的SQL语句被存储在PreparedStatement对象中,然后可以使用该
对象多次高效地执行该语句。使用PreparedStatement比使用Statement的效率要高。
创建PreparedStatement对象使用Connection的prepareStatement()方法,该方法需要传入一个SQL字符串,该SQL字符串可以包括占位符参数,如下:
//创建一个PreparedStatement对象
pstmt = conn.prepareStatement("insert into student_table values(null,?,1)");
PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三个方法来执行SQL语句,不过这三个方法无需参数,因为PreparedStatement已经存储了预
编译的SQL语句。
使用PreparedStatement预编译SQL语句时,该SQL语句可以带占位符参数,因此在执行SQL语句之前必须为这些参数传入参数值,PreparedStatement提供了一系列
的setXxx(int index, Xxx value)方法来传入参数值。
下面程序示范了使用Statement和PreparedStatement分别插入100条记录的对比。:
import java.io.FileInputStream;
import java.util.Properties;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement; public class PreparedStatementTest{
private String driver;
private String url;
private String user;
private String pass; public void initParam(String paramFile) throws Exception{
//使用Properties类加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
//加载驱动
Class.forName(driver);
} public void insertUseStatement() throws Exception{
long start = System.currentTimeMillis();
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement())
{
//需要使用100条SQL语句来插入100条记录
for(int i = 0; i < 100; i++){
stmt.executeUpdate("insert into student_table values("
+ "null,'姓名" + i + "', 1)");
}
System.out.println("使用Statement费时:"
+ (System.currentTimeMillis() - start));
}
} public void insertUsePrepare() throws Exception{
long start = System.currentTimeMillis();
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个PreparedStatement对象
PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null,?,1)"))
{
//100次为PreparedStatement的参数设值,就可以插入100条记录
for(int i = 0; i < 100; i++){
pstmt.setString(1, "姓名" + i);
pstmt.executeUpdate();
}
System.out.println("使用PreparedStatement费时:" + (System.currentTimeMillis() - start));
}
} public static void main(String[] args) throws Exception{
PreparedStatementTest pt = new PreparedStatementTest();
pt.initParam("mysql.ini");
pt.insertUseStatement();
pt.insertUsePrepare();
}
}
从上面的结果看,PreparedStatement耗时少于Statement。
使用PreparedStatement还有一个很好的作用——用于防止SQL注入。
下面以一个简单的登录窗口为例来介绍这种SQL注入的结果:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.ResultSet;
import java.util.Properties;
import java.io.FileInputStream;
import java.awt.*;
import javax.swing.*; public class LoginFrame{
private final String PROP_FILE = "mysql.ini";
private String driver;
//url是数据库的服务地址
private String url;
private String user;
private String pass;
//登录界面的GUI组件
private JFrame jf = new JFrame("登录");
private JTextField userField = new JTextField(20);
private JTextField passField = new JTextField(20);
private JButton loginButton = new JButton("登录"); public void init() throws Exception{
Properties connProp = new Properties();
connProp.load(new FileInputStream(PROP_FILE));
driver = connProp.getProperty("driver");
url = connProp.getProperty("url");
user = connProp.getProperty("user");
pass = connProp.getProperty("pass");
//加载驱动
Class.forName(driver);
//为登录按钮添加事件监听器
loginButton.addActionListener(e -> {
//登录成功则显示“登录成功”
if(validate(userField.getText(), passField.getText())){
JOptionPane.showMessageDialog(jf, "登录成功");
}else{
//否则显示“登录失败”
JOptionPane.showMessageDialog(jf, "登录失败");
}
});
jf.add(userField, BorderLayout.NORTH);
jf.add(passField);
jf.add(loginButton, BorderLayout.SOUTH);
jf.pack();
jf.setVisible(true);
} private boolean validate(String userName, String userPass){
//执行查询的SQL语句
String sql = "select * from jdbc_test "
+ "where jdbc_name='" + userName
+ "' and jdbc_desc='" + userPass + "';";
System.out.println(sql);
try(
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql))
{
//若查询的ResultSet里有超过一条的记录,则登录成功
if(rs.next()){
return true;
}
}catch(Exception e){
e.printStackTrace();
} return false;
} public static void main(String[] args) throws Exception{
new LoginFrame().init();
}
}
登录界面:
登录成功界面:
去数据库中查询是否存在用户和密码,执行的SQL语句。从上面的结果可以看出,我们在登录用户名中输入‘ or true or ’时,竟然也登录成功了。原因就出在执行的SQL语句上。
只要密码和用户为空,但是where后的条件永远为真,这就告诉软件,数据库中存在该用户,可以登录。
把上面的validate()方法换成使用PreparedStatement来执行验证,而不是直接使用Statement:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
import java.io.FileInputStream;
import java.awt.*;
import javax.swing.*; public class LoginFrame{
private final String PROP_FILE = "mysql.ini";
private String driver;
//url是数据库的服务地址
private String url;
private String user;
private String pass;
//登录界面的GUI组件
private JFrame jf = new JFrame("登录");
private JTextField userField = new JTextField(20);
private JTextField passField = new JTextField(20);
private JButton loginButton = new JButton("登录"); public void init() throws Exception{
Properties connProp = new Properties();
connProp.load(new FileInputStream(PROP_FILE));
driver = connProp.getProperty("driver");
url = connProp.getProperty("url");
user = connProp.getProperty("user");
pass = connProp.getProperty("pass");
//加载驱动
Class.forName(driver);
//为登录按钮添加事件监听器
loginButton.addActionListener(e -> {
//登录成功则显示“登录成功”
if(validate(userField.getText(), passField.getText())){
JOptionPane.showMessageDialog(jf, "登录成功");
}else{
//否则显示“登录失败”
JOptionPane.showMessageDialog(jf, "登录失败");
}
});
jf.add(userField, BorderLayout.NORTH);
jf.add(passField);
jf.add(loginButton, BorderLayout.SOUTH);
jf.pack();
jf.setVisible(true);
} private boolean validate(String userName, String userPass){
//执行查询的SQL语句
String sql = "select * from jdbc_test "
+ "where jdbc_name='" + userName
+ "' and jdbc_desc='" + userPass + "';";
System.out.println(sql);
try(
Connection conn = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = conn.prepareStatement("select * from jdbc_test where jdbc_name=? and jdbc_desc=?;"))
{
pstmt.setString(1, userName);
pstmt.setString(2, userPass);
try(
ResultSet rs = pstmt.executeQuery())
{
//若查询的ResultSet里有超过一条的记录,则登录成功
if(rs.next()){
return true;
}
}
}catch(Exception e){
e.printStackTrace();
} return false;
} public static void main(String[] args) throws Exception{
new LoginFrame().init();
}
}
登录界面:
登录失败界面:
从结果中可以看到,把用户中的' or true or '添加到了jdbc_name的后面,避免的SQL注入。
使用PreparedStatement比使用Statement多了如下三个好处:
1.PreparedStatement预编译SQL语句,性能更好
2.PreparedStatement无需“拼接”SQL语句,编程更简单
3.PreparedStatement可以防止SQL注入,安全性更好
基于上面三点,通常推荐避免使用Statement来执行SQL语句,改为使用PreparedStatement执行SQL语句。
使用PreparedStatement执行带占位符参数的SQL语句时,SQL语句中的占位符参数只能代替普通值,不能代替表名、列名等数据库对象,也不能代替的insert、select等关键字
使用CallableStatement调用存储过程:
进入一个数据库中,执行上面的命令。delimiter //将MySQL的语句结束符改为双斜线(\\),这样就可以在创建存储过程中使用分号作为分隔符(MySQL默认使用分号作为语
句结束符)。记得执行完上面命令再将结束符改为分号。上面命令创建了名为add_pro的存储过程,该存储过程包含三个参数:a b是传入参数,sum使用out修饰,是传出
参数
调用存储过程使用CallableStatement,可通过Connection的prepareCall()方法来创建CallableStatement对象,创建该对象时需要传入调用存储过程的SQL语句。
调用存储过程的SQL语句格式:{call 过程名(?, ?, ..., ?)}若下所示:
//使用Connection来创建一个CallableStatement对象
cstmt = conn.prepareCall("{call add_pro(?, ?, ?)"});
存储过程有传入参数,也有传出参数。Java程序必须为这些参数传入值,可通过CallableStatement的setXxx()方法为传入参数设置值;传出参数就是Java程序可以通过该参数
获取存储过程里的值,CallableStatement需要调用registerOutParameter()方法来注册该参数。如下所示:
//注册CallableStatement的第三个参数是int类型
cstmt.registerOutParameter(3, Types.INTEGER);
经过上面步骤,就可以调用CallableStatement的execute()方法来执行存储过程了,执行结束后通过CallableStatement对象的getXxx(int index)方法来获取指定传出参数的值。
import java.util.Properties;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.Types; public class CallableStatementTest{
private String driver;
private String url;
private String user;
private String pass; public void initParam(String paramFile) throws Exception{
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public void callProcedure()throws Exception{
//加载驱动
Class.forName(driver);
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个CallableStatement对象
CallableStatement cstmt = conn.prepareCall("{call add_pro(?,?,?)}")
){
cstmt.setInt(1, 4);
cstmt.setInt(2, 5);
//注册CallableStatement的第三个参数时int类型
cstmt.registerOutParameter(3, Types.INTEGER);
//执行存储过程
cstmt.execute();
//获取并输出存储过程传出的参数的值
System.out.println("执行结果是:" + cstmt.getInt(3));
}
} public static void main(String[] args) throws Exception{
CallableStatementTest ct = new CallableStatementTest();
ct.initParam("mysql.ini");
ct.callProcedure();
}
}
管理结果集:
JDBC使用ResultSet来封装执行查询得到的查询结果,后通过移动ResultSet的记录指针来取出结果集内容。除此之外,JDBC还允许ResultSet来更新记录,并提供
ResultSetMetaData来获取ResultSet对象的相关信息
可滚动、可更新的结果集:
使用absolute()、previous()、afterLast()等方法自由移动记录指针的ResultSet被称为可滚动的结果集。
以默认方式打开的ResultSet是不可更新的,若希望创建可更新的ResultSet,则必须在创建Statement或PreparedStatement时传入额外的参数。
Connection在创建Statement或PreparedStatement时可额外传入如下两个参数:
1.resultSetType:控制ResultSet的类型,该参数可以取如下三个值:
1.ResultSet.TYPE_FORWARD_ONLY:该常量控制记录指针只能向前移动。
2.ResultSet.TYPE_SCROLL_INSENSITIVE:该常量控制记录指针可以自由移动(可滚动结果集),但底层数据的改变不会影响ResultSet的内容
3.ResultSet.TYPE_SCROLL_SENSITIVE:该常量控制记录指针可自由移动(可滚动结果集),而且底层数据的改变会影响ResultSet的内容
TYPE_SCROLL_INSENSITIVE、TYPE_SCROLL_SENSITIVE两个常量的作用需要底层数据库驱动的支持,对于有些数据库驱动来说,这两个并没有太大的区别
2.resultSetConcurrency:控制ResultSet并发类型,该参数可以接收如下两个值:
1.ResultSet.CONCUR_READ_ONLY:该常量指示ResultSet是只读的并发模式(默认)。
2.ResultSet.CONCUR_UPDATABLE:该常量指示ResultSet是可更新的并发模式。
下面代码通过这两个参数创建了一个PreparedStatement对象,由该对象生成的ResultSet对象将是可滚动、可更新的结果集:
//使用Connection创建一个PreparedStatement对象
//传入控制结果集可滚动、可更新的参数:
pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
需要指出的是,可更新的结果集还需要满足如下两个条件:
1.所有数据都应该来自一个表
2.选出的数据集必须包含主键列
可调用ResultSet的updateXxx(intcolumnIndex, Xxx value)方法来修改记录指针所指记录、特定列的值,最后调用ResultSet的updateRow()方法来提交修改。
下面程序示范了这种创建可滚动、可更新的结果集的方法:
import java.util.Properties;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet; public class ResultSetTest{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile) throws Exception{
//使用Properties类加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public void query(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try(
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, pass);
//使用Connection来创建一个PreparedStatement对象
//传入控制结果集可滚动、可更新的参数
PreparedStatement pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery()
){
rs.last();
int rowCount = rs.getRow();
for(int i = rowCount; i > 0; i--){
rs.absolute(i);
System.out.println(rs.getString(1) + "\t"
+ rs.getString(2) + "\t" + rs.getString(3));
//修改记录指针所指记录、第2列的值
rs.updateString(2, "学生名" + i);
//提交修改
rs.updateRow();
}
}
} public static void main(String[] args) throws Exception{
ResultSetTest rt = new ResultSetTest();
rt.initParam("mysql.ini");
rt.query("select * from student_table");
}
}
student_table表中记录被倒序输出,且当程序运行结束后,student_table表中所有记录的student_name列的值都被修改。
若要创建可更新的结果集,则使用查询语句查询的数据通常只能来自于一个数据表,而且查询结果集中的数据列必须包含主键列,否则会引起更新失败。
处理Blob类型数据:
Blob(Binary Long Object):是二进制长对象,Blob列常用于存储大文件,典型的Blob内容是一张图片或一个声音文件,由于它们的特殊性,必须使用特殊的方式来存储
使用Blob列可以把图片、声音等文件的二进制数据保存在数据库中,并可以从数据库中恢复指定文件。
若需要将图片插入数据库,显然不能直接通过普通的SQL语句来完成,因为有一个关键问题——Blob常量无法表示。所以将Blob数据插入数据库需要使用
PreparedStatement,该对象有一个方法:setBinaryStream(int parameterIndex, InputStream x),该方法可以为指定参数传入二进制输入流,从而可以实现将Blob数据保存
到数据库的功能。
需要从ResultSet里取出Blob数据时,可以调用ResultSet的getBlob(int columnIndex)方法,该方法将返回一个Blob对象,Blob对象提供了getBinaryStream()方法来获取该
Blob数据的输入流,也可以使用Blob对象提供的getBytes()方法直接取出该Blob对象封装的二进制数据。
为了把图片放入数据库,使用如下SQL语句建立一个数据表:
img_data mediumblob;创建一个mediumblob类型的数据列,用于保存图片数据
mediumblob类型可存储16M内容,blob类型可存储64KB内容。
下面程序可以实现图片“上传”——实际上就是将图片保存到数据库,并在右边的列表框中显示图片的名字,当用户双击列表框中的图片名时,左边窗口将显示该图片——实质就是根据选中的ID从数据库里查找图片,并将其显示出来:
import java.sql.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Properties;
import java.util.ArrayList;
import java.io.*;
import javax.swing.filechooser.FileFilter; public class BlobTest
{
JFrame jf = new JFrame("图片管理程序");
private static Connection conn;
private static PreparedStatement insert;
private static PreparedStatement query;
private static PreparedStatement queryAll;
// 定义一个DefaultListModel对象
private DefaultListModel<ImageHolder> imageModel
= new DefaultListModel<>();
private JList<ImageHolder> imageList = new JList<>(imageModel);
private JTextField filePath = new JTextField(26);
private JButton browserBn = new JButton("...");
private JButton uploadBn = new JButton("上传");
private JLabel imageLabel = new JLabel();
// 以当前路径创建文件选择器
JFileChooser chooser = new JFileChooser(".");
// 创建文件过滤器
ExtensionFileFilter filter = new ExtensionFileFilter();
static
{
try
{
Properties props = new Properties();
props.load(new FileInputStream("mysql.ini"));
String driver = props.getProperty("driver");
String url = props.getProperty("url");
String user = props.getProperty("user");
String pass = props.getProperty("pass");
Class.forName(driver);
// 获取数据库连接
conn = DriverManager.getConnection(url , user , pass);
// 创建执行插入的PreparedStatement对象,
// 该对象执行插入后可以返回自动生成的主键
insert = conn.prepareStatement("insert into img_table"
+ " values(null,?,?)" , Statement.RETURN_GENERATED_KEYS);
// 创建两个PreparedStatement对象,用于查询指定图片,查询所有图片
query = conn.prepareStatement("select img_data from img_table"
+ " where img_id=?");
queryAll = conn.prepareStatement("select img_id, "
+ " img_name from img_table");
}
catch (Exception e)
{
e.printStackTrace();
}
}
public void init()throws SQLException
{
// -------初始化文件选择器--------
filter.addExtension("jpg");
filter.addExtension("jpeg");
filter.addExtension("gif");
filter.addExtension("png");
filter.setDescription("图片文件(*.jpg,*.jpeg,*.gif,*.png)");
chooser.addChoosableFileFilter(filter);
// 禁止“文件类型”下拉列表中显示“所有文件”选项。
chooser.setAcceptAllFileFilterUsed(false);
// ---------初始化程序界面---------
fillListModel();
filePath.setEditable(false);
// 只能单选
imageList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
JPanel jp = new JPanel();
jp.add(filePath);
jp.add(browserBn);
browserBn.addActionListener(event -> {
// 显示文件对话框
int result = chooser.showDialog(jf , "浏览图片文件上传");
// 如果用户选择了APPROVE(赞同)按钮,即打开,保存等效按钮
if(result == JFileChooser.APPROVE_OPTION)
{
filePath.setText(chooser.getSelectedFile().getPath());
}
});
jp.add(uploadBn);
uploadBn.addActionListener(avt -> {
// 如果上传文件的文本框有内容
if (filePath.getText().trim().length() > 0)
{
// 将指定文件保存到数据库
upload(filePath.getText());
// 清空文本框内容
filePath.setText("");
}
});
JPanel left = new JPanel();
left.setLayout(new BorderLayout());
left.add(new JScrollPane(imageLabel) , BorderLayout.CENTER);
left.add(jp , BorderLayout.SOUTH);
jf.add(left);
imageList.setFixedCellWidth(160);
jf.add(new JScrollPane(imageList) , BorderLayout.EAST);
imageList.addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent e)
{
// 如果鼠标双击
if (e.getClickCount() >= 2)
{
// 取出选中的List项
ImageHolder cur = (ImageHolder)imageList.
getSelectedValue();
try
{
// 显示选中项对应的Image
showImage(cur.getId());
}
catch (SQLException sqle)
{
sqle.printStackTrace();
}
}
}
});
jf.setSize(620, 400);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
// ----------查找img_table填充ListModel----------
public void fillListModel()throws SQLException
{ try(
// 执行查询
ResultSet rs = queryAll.executeQuery())
{
// 先清除所有元素
imageModel.clear();
// 把查询的全部记录添加到ListModel中
while (rs.next())
{
imageModel.addElement(new ImageHolder(rs.getInt(1)
,rs.getString(2)));
}
}
}
// ---------将指定图片放入数据库---------
public void upload(String fileName)
{
// 截取文件名
String imageName = fileName.substring(fileName.lastIndexOf('\\')
+ 1 , fileName.lastIndexOf('.'));
File f = new File(fileName);
try(
InputStream is = new FileInputStream(f))
{
// 设置图片名参数
insert.setString(1, imageName);
// 设置二进制流参数
insert.setBinaryStream(2, is , (int)f.length());
int affect = insert.executeUpdate();
if (affect == 1)
{
// 重新更新ListModel,将会让JList显示最新的图片列表
fillListModel();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
// ---------根据图片ID来显示图片----------
public void showImage(int id)throws SQLException
{
// 设置参数
query.setInt(1, id);
try(
// 执行查询
ResultSet rs = query.executeQuery())
{
if (rs.next())
{
// 取出Blob列
Blob imgBlob = rs.getBlob(1);
// 取出Blob列里的数据
ImageIcon icon=new ImageIcon(imgBlob.getBytes(1L
,(int)imgBlob.length()));
imageLabel.setIcon(icon);
}
}
}
public static void main(String[] args)throws SQLException
{
new BlobTest().init();
}
}
// 创建FileFilter的子类,用以实现文件过滤功能
class ExtensionFileFilter extends FileFilter
{
private String description = "";
private ArrayList<String> extensions = new ArrayList<>();
// 自定义方法,用于添加文件扩展名
public void addExtension(String extension)
{
if (!extension.startsWith("."))
{
extension = "." + extension;
extensions.add(extension.toLowerCase());
}
}
// 用于设置该文件过滤器的描述文本
public void setDescription(String aDescription)
{
description = aDescription;
}
// 继承FileFilter类必须实现的抽象方法,返回该文件过滤器的描述文本
public String getDescription()
{
return description;
}
// 继承FileFilter类必须实现的抽象方法,判断该文件过滤器是否接受该文件
public boolean accept(File f)
{
// 如果该文件是路径,接受该文件
if (f.isDirectory()) return true;
// 将文件名转为小写(全部转为小写后比较,用于忽略文件名大小写)
String name = f.getName().toLowerCase();
// 遍历所有可接受的扩展名,如果扩展名相同,该文件就可接受。
for (String extension : extensions)
{
if (name.endsWith(extension))
{
return true;
}
}
return false;
}
}
// 创建一个ImageHolder类,用于封装图片名、图片ID
class ImageHolder
{
// 封装图片的ID
private int id;
// 封装图片的图片名字
private String name;
public ImageHolder(){}
public ImageHolder(int id , String name)
{
this.id = id;
this.name = name;
}
// id的setter和getter方法
public void setId(int id)
{
this.id = id;
}
public int getId()
{
return this.id;
}
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// 重写toString方法,返回图片名
public String toString()
{
return name;
}
}
使用ResultSetMetaData分析结果集:
当执行SQL查询后可以通过移动记录指针来遍历ResultSet的每条记录,但程序可能不清楚该ResultSet里包含哪些数据列,以及每个数据列的数据类型,那么可以通
过ResultSetMetaData来获取关于ResultSet的描述信息:
MetaData的意思是元数据,即描述其他数据的数据,因此ResultSetMetaData封装了描述ResultSet对象的数据;后面还要介绍的DatabaseMetaData则封装了描述
Database的数据。
ResultSet中包含了一个getMetaData()方法,该方法可以返回该ResultSet对应的ResultSetMetaData对象。一旦获得了ResultSetMetaData对象就可以通过
ResultSetMetaData提供的大量方法来返回ResultSet的描述信息。常用方法有如下三个:
1.int getColumnCount():返回该ResultSet的列数量
2.String getColumnName(int Column):返回指定索引的列名
3.int getColumnType(int column):返回指定索引的列类型
下面是一个简单的查询器,当用户在文本框内输入合法的查询语句并执行成功后,下面表格将会显示查询结果:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import java.util.*;
import java.io.*;
import java.sql.*; public class QueryExecutor
{
JFrame jf = new JFrame("查询执行器");
private JScrollPane scrollPane;
private JButton execBn = new JButton("查询");
// 用于输入查询语句的文本框
private JTextField sqlField = new JTextField(45);
private static Connection conn;
private static Statement stmt;
// 采用静态初始化块来初始化Connection、Statement对象
static
{
try
{
Properties props = new Properties();
props.load(new FileInputStream("mysql.ini"));
String drivers = props.getProperty("driver");
String url = props.getProperty("url");
String username = props.getProperty("user");
String password = props.getProperty("pass");
// 加载数据库驱动
Class.forName(drivers);
// 取得数据库连接
conn = DriverManager.getConnection(url, username, password);
stmt = conn.createStatement();
}
catch (Exception e)
{
e.printStackTrace();
}
}
// --------初始化界面的方法---------
public void init()
{
JPanel top = new JPanel();
top.add(new JLabel("输入查询语句:"));
top.add(sqlField);
top.add(execBn);
// 为执行按钮、单行文本框添加事件监听器
execBn.addActionListener(new ExceListener());
sqlField.addActionListener(new ExceListener());
jf.add(top , BorderLayout.NORTH);
jf.setSize(680, 480);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
// 定义监听器
class ExceListener implements ActionListener
{
public void actionPerformed(ActionEvent evt)
{
// 删除原来的JTable(JTable使用scrollPane来包装)
if (scrollPane != null)
{
jf.remove(scrollPane);
}
try(
// 根据用户输入的SQL执行查询
ResultSet rs = stmt.executeQuery(sqlField.getText()))
{
// 取出ResultSet的MetaData
ResultSetMetaData rsmd = rs.getMetaData();
Vector<String> columnNames = new Vector<>();
Vector<Vector<String>> data = new Vector<>();
// 把ResultSet的所有列名添加到Vector里
for (int i = 0 ; i < rsmd.getColumnCount(); i++ )
{
columnNames.add(rsmd.getColumnName(i + 1));
}
// 把ResultSet的所有记录添加到Vector里
while (rs.next())
{
Vector<String> v = new Vector<>();
for (int i = 0 ; i < rsmd.getColumnCount(); i++ )
{
v.add(rs.getString(i + 1));
}
data.add(v);
}
// 创建新的JTable
JTable table = new JTable(data , columnNames);
scrollPane = new JScrollPane(table);
// 添加新的Table
jf.add(scrollPane);
// 更新主窗口
jf.validate();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
new QueryExecutor().init();
}
}
虽然ResultSetMetaData可以准确的分析出ResultSet里包含多少列,以及每列的列名、数据类型等,但使用ResultSetMetaData需要一定的系统开销,因此若在编程过程中
已经知道ResultSet里包含多少列,以及每列的列名、类型等信息,就没有必要使用ResultSetMetaData来分析该ResultSet对象了。
Java7的RowSet1.1:
RowSet接口继承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet和WebRowSet常用子接口。除了JdbcRowSet需要保
持与数据库连接之外,其余4个子接口都是离线的RowSet,无需保持与数据库的连接。
与ResultSet相比,RowSet默认是可滚动、可更新、可序列化的结果集,而且作为JavaBean使用,因此能方便地在网络上传输,用于同步两端的数据。对于离线RowSet而
言,程序在创建RowSet时已经把数据从底层数据库读取到内存,因此可以充分利用计算机内存,从而降低数据库服务器的负载,提高程序性能。
Java7新增的RowSetFactory与RowSet:
Java7新增了RowSetProvider类和RowSetFactory接口,其中RowSetProvider负责创建RowSetFactory,而RowSetFactory则提供了如下方法来创建RowSet实例:
1.CachedRowSet createCachedRowSet():创建一个默认的CachedRowSet。
2.FilteredRowSet createFilteredRowSet():创建一个默认的FilteredRowSet。
3.JdbcRowSet createJdbcRowSet():创建一个默认的JdbcRowSet。
4.JoinRowSet createJoinRowSet():创建一个默认的JoinRowSet。
5.WebRowSet createWebRowSet():创建一个默认的WebRowSet。
通过使用RowSetFactory,就可以把应用程序与RowSet实现类分离开,有利于后期的升级、扩展。
下面使用RowSetFactory来创建JdbcRowSet实例:
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.rowset.*; public class RowSetFactoryTest
{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile)throws Exception
{
// 使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public void update(String sql)throws Exception
{
// 加载驱动
Class.forName(driver);
// 使用RowSetProvider创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
try(
// 使用RowSetFactory创建默认的JdbcRowSet实例
JdbcRowSet jdbcRs = factory.createJdbcRowSet())
{
// 设置必要的连接信息
jdbcRs.setUrl(url);
jdbcRs.setUsername(user);
jdbcRs.setPassword(pass);
// 设置SQL查询语句
jdbcRs.setCommand(sql);
// 执行查询
jdbcRs.execute();
jdbcRs.afterLast();
// 向前滚动结果集
while (jdbcRs.previous())
{
System.out.println(jdbcRs.getString(1)
+ "\t" + jdbcRs.getString(2)
+ "\t" + jdbcRs.getString(3));
if (jdbcRs.getInt("student_id") == 3)
{
// 修改指定记录行
jdbcRs.updateString("student_name", "孙悟空");
jdbcRs.updateRow();
}
}
}
}
public static void main(String[] args)throws Exception
{
RowSetFactoryTest jt = new RowSetFactoryTest();
jt.initParam("mysql.ini");
jt.update("select * from student_table");
}
}
上面程序使用RowSetFactory来创建JdbcRowSet对象。由于通过这种方式创建的JdbcRowSet还没有传入Connection参数,因此程序还需调用setUrl()、setUsername()、setPassword()等方法来设置数据库连接信息。
离线RowSet:
在使用ResultSet的时代,程序查询得到ResultSet之后必须立即读取或处理它对应的记录,否则一旦关闭Connection,再通过ResultSet读取记录就会引发异常。
离线RowSet会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象则完全可以当成JavaBean来使用,因此不仅安全,且编程十分简单。CachedRowSet是
所有离线RowSet的父接口。
下面以CachedRowSet为例进行介绍:
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.*;
import javax.sql.rowset.*; public class CachedRowSetTest
{
private static String driver;
private static String url;
private static String user;
private static String pass;
public void initParam(String paramFile)throws Exception
{
// 使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public CachedRowSet query(String sql)throws Exception
{
// 加载驱动
Class.forName(driver);
// 获取数据库连接
Connection conn = DriverManager.getConnection(url , user , pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 使用RowSetProvider创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
// 创建默认的CachedRowSet实例
CachedRowSet cachedRs = factory.createCachedRowSet();
// 使用ResultSet装填RowSet
cachedRs.populate(rs); // ①
// 关闭资源
rs.close();
stmt.close();
conn.close();
return cachedRs;
}
public static void main(String[] args)throws Exception
{
CachedRowSetTest ct = new CachedRowSetTest();
ct.initParam("mysql.ini");
CachedRowSet rs = ct.query("select * from student_table");
rs.afterLast();
// 向前滚动结果集
while (rs.previous())
{
System.out.println(rs.getString(1)
+ "\t" + rs.getString(2)
+ "\t" + rs.getString(3));
if (rs.getInt("student_id") == 3)
{
// 修改指定记录行
rs.updateString("student_name", "孙悟空");
rs.updateRow();
}
}
// 重新获取数据库连接
Connection conn = DriverManager.getConnection(url
, user , pass);
conn.setAutoCommit(false);
// 把对RowSet所做的修改同步到底层数据库
rs.acceptChanges(conn);
}
}
从上面程序可以看到在Connection关闭的情况下,程序依然可以读取、修改RowSet里的记录。为了将程序对离线RowSet所做的修改同步到底层数据库,程序在调用RowSet的
acceptChanges()方法时,必须传入Connection。
离线RowSet的查询分页:
由于CachedRowSet会将数据记录直接装载到内存中,若SQL查询返回的记录过大,CachedRowSet将会占用大量内存,在某些极端情况下,将会导致内存溢出。
未解决上述问题,CachedRowSet提供了分页功能。即一次只装载ResultSet里的某几条记录,这样就避免了CachedRowSet占用内存过大的问题。
CachedRowSet提供了如下方法控制分页:
1.populate(ResultSet rs, int startRow):使用给定的ResultSet装填RowSet,从ResultSet的第startRow条记录开始装填
2.setPageSize(int pageSize):设置CachedRowSet每次返回多少条记录
3.previousPage():在底层ResultSet可用的情况下,让CachedRowSet读取上一页记录。
4.nextPage():在底层ResultSet可用的情况下,让CachedRowSet读取下一页记录
下面程序示范了CachedRowSet的分页支持:
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.*;
import javax.sql.rowset.*; public class CachedRowSetPage
{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile)throws Exception
{
// 使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
} public CachedRowSet query(String sql , int pageSize
, int page)throws Exception
{
// 加载驱动
Class.forName(driver);
try(
// 获取数据库连接
Connection conn = DriverManager.getConnection(url , user , pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql))
{
// 使用RowSetProvider创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
// 创建默认的CachedRowSet实例
CachedRowSet cachedRs = factory.createCachedRowSet();
// 设置每页显示pageSize条记录
cachedRs.setPageSize(pageSize);
// 使用ResultSet装填RowSet,设置从第几条记录开始
cachedRs.populate(rs , (page - 1) * pageSize + 1);
return cachedRs;
}
}
public static void main(String[] args)throws Exception
{
CachedRowSetPage cp = new CachedRowSetPage();
cp.initParam("mysql.ini");
CachedRowSet rs = cp.query("select * from student_table" , 3 , 2); // ①
// 向后滚动结果集
while (rs.next())
{
System.out.println(rs.getString(1)
+ "\t" + rs.getString(2)
+ "\t" + rs.getString(3));
}
}
}
程序中要查询第2页的记录,每页显示3条记录。
事务处理:
事物的概念和MySQL事务支持:
事务是由一步或几步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。
事务具备四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性(Durability)。简称ACID特性。
JDBC的事务支持:
JDBC连接的事务支持有Connection提供,Connection默认打开自动提交,即关闭事务。这种情况下,每一条SQL语句一旦执行,便会立即提交到数据库,永久生效,无法
对其进行回滚操作。
可调用Connection的setAutoCommit()方法来关闭自动提交,开启事务:
//conn.setAutoCommit(false);
等到所有SQL语句都被执行,程序可以调用Connection的commit()方法来提交事务:
//conn.commit();
若任意一条SQL语句执行失败,则应该用Connection的rollback()方法来回滚事务:
//conn.rollback();
实际上,当Connection遇到一个未处理的SQLException异常时,系统将会非正常退出,事务也会自动回滚。但若程序捕获了该异常,则需要在异常处理块中显式地回滚
事务
import java.sql.*;
import java.io.*;
import java.util.*; public class TransactionTest
{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile)throws Exception
{
// 使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
}
public void insertInTransaction(String[] sqls) throws Exception
{
// 加载驱动
Class.forName(driver);
try(
Connection conn = DriverManager.getConnection(url , user , pass))
{
// 关闭自动提交,开启事务
conn.setAutoCommit(false);
try(
// 使用Connection来创建一个Statment对象
Statement stmt = conn.createStatement())
{
// 循环多次执行SQL语句
for (String sql : sqls)
{
stmt.executeUpdate(sql);
}
}
// 提交事务
conn.commit();
}
}
public static void main(String[] args) throws Exception
{
TransactionTest tt = new TransactionTest();
tt.initParam("mysql.ini");
String[] sqls = new String[]{
"insert into student_table values(null , 'aaa' ,1)",
"insert into student_table values(null , 'bbb' ,1)",
"insert into student_table values(null , 'ccc' ,1)",
// 下面这条SQL语句将会违反外键约束,
// 因为teacher_table中没有ID为5的记录。
"insert into student_table values(null , 'ccc' ,5)" //①
};
tt.insertInTransaction(sqls);
}
}
上面代码报错会因为插入语句第四条有错。正是因为这条语句出错,导致产生异常,且该异常没有得到处理,引起程序非正常结束,所以事务自动回滚,上面3条插入语句无效。
Connection也提供了设置中间点的方法:
1.Savepoint setSavepoint():在当前事务中创建一个未命名的中间点,并返回代表该中间点的Savepoint对象
2.Savepoint setSavepoint(String name):在当前事务中创建一个具有指定名称的中间点,并返回代表该中间点的SavepointSavepoint对象。
通常来说设置中间点时,没有必要指定名称,因为Connection回滚到指定中间点时,并不是根据名字回滚的,而是根据中间点对象回滚的,Connection提供了
rollback(Savepoint savepoint)方法回滚到指定中间点。
Java8增强的批量更新:
JDBC还提供了一个批量更新的功能,批量更新时,多条SQL语句将被作为一批操作被同时收集,并同时提交。
批量更新必须得到底层数据库的支持,可以通过调用DatabaseMetaData的supportsBatchUpdates()方法来查看底层数据库是否支持批量更新。
使用批量更新需要先创建一个Statement对象,然后利用该对象的addBatch()方法将多条SQL语句同时收集,最后调用Java8位Statement对象新增的executeLargeBatch()或
原有的executeBatch()方法同时执行这些SQL语句。只要批量操作中任何一条SQL语句影响的记录条数可能超过Integer.MAX_VALUE,就应该使用executeLargeBatch()方
法。如下:
Statement stmt = conn.createStatement();
//使用Statement同时收集多条SQL语句
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.addBatch(sql3);
...
//同时执行所有的SQL语句
stmt.executeLargeBatch();
若在批量更新的addBatch()方法中添加了select查询语句,程序将会直接出现错误。为了让批量操作可以正确的处理错误,必须把批量执行的操作视为单个事务,若批量更
新在执行过程中失败,则让事务回滚到批量操作开始之前的状态。为达到这种效果,程序应该在开始批量操作之前先关闭自动提交,然后开始收集更新语句,当批量操作
执行结束后,提交事务,并恢复之前的自动提交模式,如下:
//保存当前的自动的提交模式
boolean autoCommit = conn.getAutoCommit();
//关闭自动提交
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
//使用Statement同时收集多条SQL语句
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.addBatch(sql3);
...
//同时执行所有的SQL语句
stmt.executeLargeBatch();
//提交修改
conn.commit();
//恢复原有的紫东提交模式
conn.setAutocommit(autoCommit);
MySQL的最新驱动依然不支持executeLargeBatch()方法,对于数据库驱动不支持executeLargeBatch()的情形,则只能依然使用传统的executeBatch()方法。
分析数据库信息:
使用DatabaseMetaData分析数据库信息:
JDBC提供了DatabaseMetaData来封装数据库连接对应数据库的信息,通过Connection提供的getMetaData()方法就可以获取数据库对应的DatabaseMetaData对象
DatabaseMetaData接口通常由驱动程序供应商提供实现,其目的是让用户了解底层数据库的相关信息。使用该接口的目的是发现如何处理底层数据库,尤其是对于试图与
多个数据库一起使用的应用程序——因为应用程序需要在多个数据库之间切换,所以必须利用该接口来找出底层数据库的功能,如:调用supportsCorrelatedSubqueries
()方法查看是否可以使用关联子查询,或者调用supportsBatchUpdates()方法查看是否可以使用批量更新。
许多DatabaseMetaData方法以ResultSet对象的形式返回查询信息,然后使用ResultSet的常规方法(如:getString()和getInt())即可从这些ResultSet对象中获取数据。若
查询的信息不可用,则将返回一个空ResultSet对象。
DatabaseMetaData的很多方法都需要传入一个XXXPattern模式字符串,这里的XXXPattern不是正则表达式,而是SQL里的模式字符串,即用%代表任意多个字符,使用下
划线代表一个字符。在通常情况下,若把该模式字符串的参数值设置为null,即表明该参数不作为过滤条件。
下面程序通过DatabaseMetaData分析了当前Connection连接对应数据库的一些基本信息,包括当前数据库包含多少数据表,存储过程,student_table表的数据列、主键、
外键等信息:
import java.sql.*;
import java.util.*;
import java.io.*; public class DatabaseMetaDataTest
{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile)throws Exception
{
// 使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
}
public void info() throws Exception
{
// 加载驱动
Class.forName(driver);
try(
// 获取数据库连接
Connection conn = DriverManager.getConnection(url
, user , pass))
{
// 获取的DatabaseMetaData对象
DatabaseMetaData dbmd = conn.getMetaData();
// 获取MySQL支持的所有表类型
ResultSet rs = dbmd.getTableTypes();
System.out.println("--MySQL支持的表类型信息--");
printResultSet(rs);
// 获取当前数据库的全部数据表
rs = dbmd.getTables(null,null, "%" , new String[]{"TABLE"});
System.out.println("--当前数据库里的数据表信息--");
printResultSet(rs);
// 获取student_table表的主键
rs = dbmd.getPrimaryKeys(null , null, "student_table");
System.out.println("--student_table表的主键信息--");
printResultSet(rs);
// 获取当前数据库的全部存储过程
rs = dbmd.getProcedures(null , null, "%");
System.out.println("--当前数据库里的存储过程信息--");
printResultSet(rs);
// 获取teacher_table表和student_table之间的外键约束
rs = dbmd.getCrossReference(null,null, "teacher_table"
, null, null, "student_table");
System.out.println("--teacher_table表和student_table之间"
+ "的外键约束--");
printResultSet(rs);
// 获取student_table表的全部数据列
rs = dbmd.getColumns(null, null, "student_table", "%");
System.out.println("--student_table表的全部数据列--");
printResultSet(rs);
}
}
public void printResultSet(ResultSet rs)throws SQLException
{
ResultSetMetaData rsmd = rs.getMetaData();
// 打印ResultSet的所有列标题
for (int i = 0 ; i < rsmd.getColumnCount() ; i++ )
{
System.out.print(rsmd.getColumnName(i + 1) + "\t");
}
System.out.print("\n");
// 打印ResultSet里的全部数据
while (rs.next())
{
for (int i = 0; i < rsmd.getColumnCount() ; i++ )
{
System.out.print(rs.getString(i + 1) + "\t");
}
System.out.print("\n");
}
rs.close();
}
public static void main(String[] args)
throws Exception
{
DatabaseMetaDataTest dt = new DatabaseMetaDataTest();
dt.initParam("mysql.ini");
dt.info();
}
}
结果太多,只截取一部分。
使用系统表分析数据库信息:
除了DatabaseMetaData来分析底层数据库信息之外,若已经确定应用程序所以用的数据库系统,则可以通过数据库的系统来分析数据库信息。
系统表又称为数据字典,数据字典的数据通常由数据库系统负责维护,用户通常只能查询数据字典,而不能修改数据字典的内容。
MySQL数据库使用information_schema数据库来保存系统表,在数据库里包含了大量系统表,常用系统表的简单介绍如下:
1.tables:存放数据库里所有数据表信息
2.schemata:存放数据库里所有数据库的信息
3.views:存放数据库里所有视图的信息
4.columns:存放数据库里所有列的信息
5.triggers:存放数据库里所有触发器的信息
6.routines:存放数据库里所有存储过程和函数的信息
7.key_column_usage:存放数据库里所有具有约束的键信息
8.table_constraints:存放数据库里全部约束表的信息
9.statistics:存放数据库里全部索引的信息
使用连接池管理连接:
数据库连接的建立和关闭是极耗费系统资源的操作,数据库连接池的解决方案是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池。每次应
用程序请求数据库连接是,无需重新打开连接,而是从连接池中取出已有的连接使用,使用完后不再关闭数据库连接,而是直接将连接归还给连接池。
对于共享资源的情况,有一个通用的设计模式:资源池(Resource Pool),用于解决资源的频繁请求、释放所造成的性能下降。
数据库连接池是Connection对象的工厂,数据库连接池的常用参数如下:
1.数据库的初始连接数
2.连接池的最大连接数
3.连接池的最小连接数
4.连接池每次增加的容量
JDBC的数据库连接池使用 javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由商用服务器提供实现,也有一些开源组织提供实现(如DBCP和C3P0)。
DBCP数据源:
DBCP是Apache软件基金组织下的开源连接实现,该连接池依赖该组织下的另一个开源系统:common-pool。若需要使用该连接池实现,则应在系统中增加两个jar 文件:
1.commons-dbcp.jar:连接池的实现
2.commons-pool.jar:连接池实现的依赖库
Tomcat的连接池正是采用该连接池实现的。数据库连接池既可以与应用服务器整合使用,也可以由应用程序独立使用。
下面代码片段示范了使用DBCP来获得数据库连接方式:
//创建连接池实例
BasicDataSource ds = new BasicDataSourc();
//设置连接池所需驱动
ds.setDriverClassName("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds.setUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUsername("root");
//设置连接数据库的密码
ds.setPassword("pass");
//设置连接池的初始连接数
ds.setInitialSize(5);
//设置连接池最多可有多少个活动连接数
ds.setMaxActive(20);
//设置连接池中最少有2个空闲的连接
ds.setMinIdle(2);
数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。即:一个应用,上面代码只需要执行一次即可。
建议把上面程序中的ds设置成static成员变量,并且在应用开始时立即初始化数据源对象,程序中所有需要获取数据库连接的地方直接访问该ds对象,并获取数据库连接即
可。
//通过数据源获取数据库连接
Connection conn = ds.getConnection();
当数据库访问结束后,程序还是像以前一样关闭数据库连接:
//释放数据库连接
conn.close();
C3P0数据源:
C3P0数据源性能更胜一筹,Hibernate就推荐使用该连接池。C3P0连接池不仅可以自动清理不在使用的Connection,还可以自动清理ResultSet和Statement。
若需要使用C3P0连接池,则应在系统中增加如下JAR文件
1.c3p0-0.9.1.2.jar:C3P0连接池的实现
下面代码通过C3P0连接池获得数据库连接:
//创建连接池实例
ComboPooledDataSource ds = new ComboPooledDataSource();
//设置连接池所需驱动
ds.setDriverClass("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUser("root");
//设置连接数据库的密码
ds.setPassword("pass");
//设置连接池的最大连接数
ds.setMaxPoolSize(40);
//设置连接池的最小连接数
ds.setMinPoolSIze(2);
//设置连接池的初始连接数
ds.setInitialPoolSize(10);
//设置连接池的缓存Statement的最大数
ds.setMaxStatements(180);
通过如下代码获取数据库连接:
Connection conn = ds.getConnection();