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();

第十三章.MySQL数据库与JDBC编程(下)的更多相关文章

  1. MySql数据库与JDBC编程

    JDBC -- Java Database Connectivity,即Java数据库连接,通过使用JDBC就可以使用同一种API访问不同的数据库 SQL语句基础(SQL结构化查询语言) 能完成的任务 ...

  2. MySql数据库与JDBC编程二

    DML语法语句:主要操作数据表中的数据,完成插入新数据,修改已有数据,删除不要的数据的任务 1,insert into 语句 用于向指定表插入数据,一次只能插入一条记录:insert into tab ...

  3. MySql数据库与JDBC编程三

    多表连接查询(两种规范 SQL92和SQL99) SQL92规范: 等值连接,非等值连接,外连接,广义笛卡儿积连接 多个表都放在from后,,连接条件放在where后,条件要求两列值相等,则为等值连接 ...

  4. MySQL性能调优与架构设计——第9章 MySQL数据库Schema设计的性能优化

    第9章 MySQL数据库Schema设计的性能优化 前言: 很多人都认为性能是在通过编写代码(程序代码或者是数据库代码)的过程中优化出来的,其实这是一个非常大的误区.真正影响性能最大的部分是在设计中就 ...

  5. MySQL性能调优与架构设计——第8章 MySQL数据库Query的优化

    第8章 MySQL数据库Query的优化 前言: 在之前“影响 MySQL 应用系统性能的相关因素”一章中我们就已经分析过了Query语句对数据库性能的影响非常大,所以本章将专门针对 MySQL 的 ...

  6. MySQL性能调优与架构设计——第7章 MySQL数据库锁定机制

    第7章 MySQL数据库锁定机制 前言: 为了保证数据的一致完整性,任何一个数据库都存在锁定机制.锁定机制的优劣直接应想到一个数据库系统的并发处理能力和性能,所以锁定机制的实现也就成为了各种数据库的核 ...

  7. 基于Mysql数据库亿级数据下的分库分表方案

    移动互联网时代,海量的用户数据每天都在产生,基于用户使用数据的用户行为分析等这样的分析,都需要依靠数据都统计和分析,当数据量小时,问题没有暴露出来,数据库方面的优化显得不太重要,一旦数据量越来越大时, ...

  8. MySQL数据库的优化(下)MySQL数据库的高可用架构方案

    MySQL数据库的优化(下)MySQL数据库的高可用架构方案 2011-03-09 08:53 抚琴煮酒 51CTO 字号:T | T 在上一篇MySQL数据库的优化中,我们跟随笔者学习了单机MySQ ...

  9. “全栈2019”Java第五十三章:向上转型和向下转型详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

随机推荐

  1. XML标签属性

    0. 内容索引 1. XML序列化与反序列化 2. 相关的常用Attribute 3. 相关的全部Attribute 4. XML序列化答疑 5. 高级议题 1. XML序列化与反序列化 // OBJ ...

  2. Python3之文本操作

    文件操作示例分析: 文件操作一般要经历三个步骤: 打开文件 操作文件 关闭文件 读取操作示例: >>>f = open('test.txt', 'r') # 打开文件test.txt ...

  3. curl命令上传下载文件

    下载单个文件,默认将输出打印到标准输出中(STDOUT)中 curl http://www.centos.org 通过-o/-O选项保存下载的文件到指定的文件中: -o:将文件保存为命令行中指定的文件 ...

  4. git 克隆 提交本地修改到远程方法

    最近一个项目,提交总报错 按照下面的流程就ok了 $ git clone $ git init $ cd shop $ git branch -al //查看所有分支 $ git pull origi ...

  5. 3.2、Factorization Machine实践

    1.在上一篇博客中我们构建度为二的因子分解机模型,这篇博客对这个模型进行实践 下图为准备的数据集: 完整代码为: # -*- coding: UTF-8 -*- # date:2018/6/6 # U ...

  6. Springboot第二篇:与前端fetch通信(附springboot解决跨域方法)

    说到与前端通信,明白人都知道这章肯定会写两部分的东西啦. 关于后台 ①首先回顾前文,上一章环境搭建如图: ②我们在maven.example.controller下添加一个文件,并附上如图代码: ③: ...

  7. odoo 的一些orm 操作

    1.基础文件及目录结构 在认识odoo ORM框架前,先介绍一下odoo中模块目录结构.     data:存放模块预制数据 i18n:存放国际化文件 models:存放模型等py代码 securit ...

  8. 面经问题总结——django相关

    1.让你从头设计一个web框架,第一步你会做什么? 2.django的orm是怎么实现的? 3.django的URL路径映射是怎么实现的? 4.平常你怎么运用django提供的钩子函数? 5.三级分销 ...

  9. 查看SqlServer某张表的物理空间占用情况

    以下语句可以查看表的物理空间使用情况 包括 [ROWS] 内容的行数.. [reserved] 保留的磁盘大小.. [data] 数据占用的磁盘大小.. [index_size] 索引占用的磁盘大小. ...

  10. Go语言小试牛刀---几个简单的例子

    整理资料,发现之前手写的Go语言资料,现在贴过来. 第一个:Channel的使用,创建一个随机数 package main import "fmt" import "ru ...