一.单元概述
现在每一个人的生活几乎都离不开数据库,如果没有数据库,很多事情就会变得非常棘手,也许根本无法做到。银行、大学和图书馆就是几个严重依赖数据库系统的地方。在互联网上,使用搜索引擎、在线购物甚至是访问网站都离不开数据库。数据库通常都安装在称为数据库服务器的计算机上。
学习Java语言的数据库编程,就必须学习JDBC技术,因为JDBC技术是Java语言中被广泛使用的一种操作数据库的技术。JDBC是一套基于Java技术的数据库编程接口,它由一些操作数据库的Java类和接口组成。用JDBC编写访问数据库的程序,可以实现应用程序与数据库的无关性,即设计人员不用关心系统使用的是什么数据库管理系统,只要数据库厂商提供了该数据库的JDBC驱动程序,就可以在任何一种数据库系统中使用。本章介绍JDBC的概念、工作原理和在Java程序中访问数据库的方法。
二,教学重点与难点
重点:
(1) JDBC概述
(2) 创建JDBC应用
(3) PreparedStatement
(4) 事务
难点:
(1) PreparedStatement
(2) 事务
7.1JDBC概述
7.1.1什么是JDBC
JDBC提供了在Java程序中直接访问数据库的功能,那么,什么是JDBC?JDBC是如何工作的?本节将介绍JDBC的这些基本知识。
JDBC(Java DataBase Connectivity) 称为Java数据库连接,它是一种用于数据库访问的应用程序API,由一组用Java语言编写的类和接口组成,有了JDBC就可以用统一的语法对多种关系数据库进行访问,而不用担心其数据库操作语言的差异。通过JDBC提供的方法,用户能够以一致的方式连接多种不同的数据库系统,而不必再为每一种数据库系统编写不同的Java程序代码。JDBC连接数据库之前必须先装载特定厂商提供的数据库驱动程序(Driver),通过JDBC通用API访问数据库。有了JDBC,就不必为访问Mysql数据库专门写一个程序,为访问Oracle又专门写一个程序等等,如图7-1所示。
7.1.2 JDBC的体系结构
JDBC的结构可划分为两层:一个是面向底层的JDBC Driver Interface(驱动程序管理器接口),另一个是面向程序员的JDBC API,如图7-2所示。
使用JDBC编程,可让开发人员从复杂的驱动器调用命令和函数中解脱出来,可以致力于应用程序中的关键地方。JDBC支持不同的关系数据库,这使得程序的可移植性大大加强。JDBC API是面向对象的,可以让用户把常用的方法封装为—个类,以备后用。但是它也有缺点,一是使用JDBC,访问数据记录的速度会受到一定程度的影响。二是JDBC结构中包含不同厂家的产品,这就给更改数据源带来了很大的麻烦。
7.1.3 JDBC核心接口与类
JDBC核心类库包含在java.sql包中,主要类有:
- DriverManager:负责管理JDBC驱动程序。使用JDBC驱动程序之前,必须先将驱动程序加载并注册后才可以使用,同时提供方法来建立与数据库的连接。
- SQLException:有关数据库操作的异常。
主要接口有:
- Connection:特定数据库的连接,在连接上下文中执行SQL语句并返回结果。
- Statement:用于执行静态SQL语句并返回它所生成结果的对象。
- PreparedStatement:表示预编译的SQL语句的对象,继承并扩展了Statement接口。
- CallableStatement:用于执行SQL存储过程的接口,继承并扩展了PreparedStatement接口。
- ResultSet:装载数据库结果集的接口。
7.2 创建JDBC应用
7.2.1 创建JDBC应用程序的步骤
使用JDBC操作数据库中的数据包括6个基本操作步骤:
(1)、载入JDBC驱动程序:
首先要在应用程序中加载驱动程序driver,使用Class.forName()方法加载特定的驱动程序,每种数据库管理系统的驱动程序不同,由数据库厂商提供。
(2)、定义连接URL,建立数据库连接对象:
通过DriverManager类的getConnection()方法获得表示数据库连接的Connection类对象。
(3)创建Statement对象:获取Connection对象以后,可以用Connection对象的方法创建一个Statement对象的实例。
(4)、执行查询或更新:
Statement对象可以执行SELECT语句的executeQuery()方法或执行INSERT、UPDATE、DELETE语句的executeUpdate()方法。
(5)、操作结果集:
利用ResultSet对象对数据库操作返回的结果进行处理。ResultSet包含一些用来从结果集中获取数据并保存到Java变量中的方法。主要包括next()方法,用于移动结果集游标,逐行处理结果集,getString()、getInt()、getDate()、getDouble()等方法,用于将数据库中的数据类型转换为Java的数据类型。
(6)、释放资源:
使用与数据库相关的对象非常耗内存,因此在数据库访问后要关闭与数据库的连接,同时还应该关闭ResultSet、Statement和Connection等对象。可以使用每个对象自己的close()方法完成。
下面,就详细创建JDBC应用程序的详细步骤:
(1)、载入JDBC驱动程序
目前比较常见的JDBC驱动程序可分为以下四个种类:
a)JDBC-ODBC桥加上ODBC驱动程序
把JDBC API调用转换成ODBC API调用,然后ODBC API调用针对供应商的ODBC驱动程序来访问数据库,即利用JDBC-ODBC桥通过ODBC来操作数据源。
b)本地API
这种类型的驱动程序把客户机API上的JDBC调用转换为Oracle、Sybase、Informix、DB2或其它DBMS的调用。这种驱动方式将数据库厂商的特殊协议转换成Java代码及二进制类码,使Java 数据库客户方与数据库服务器方通信。
c) JDBC网络纯Java驱动程序
这种驱动程序将JDBC转换为与DBMS无关的网络协议,之后这种协议又被某个服务器转换为一种DBMS协议。服务器中间件能够将它的纯Java客户机连接到多种不同的数据库上。数据库客户以标准网络协议(如HTTP、SHTTP)同数据库访问服务器通信,数据库访问服务器然后翻译标准网络协议成为数据库厂商的专有特殊数据库访问协议与数据库通信。
d) 本地协议纯Java驱动程序
这种类型的驱动程序将JDBC调用直接转换为DBMS所使用的网络协议。这将允许从客户机机器上直接调用DBMS服务器。这种方式也是纯Java driver。数据库厂商提供了特殊的JDBC协议使Java数据库客户与数据库服务器通信。这种驱动直接把JDBC调用转换为符合相关数据库系统规范的请求。由于第四种类型驱动写的应用可以直接和数据库服务器通讯,这种类型的驱动完全由Java实现,因此实现了平台独立性。
四种类型驱动程序如图7-3所示,通常开发中多采用第四种方式,这种驱动不需要先把JDBC的调用传给ODBC或本地数据库接口或者是中间层服务器,所以它的执行效率高。各数据库厂商均提供对JDBC的支持,即提供数据库连接使用的驱动程序文件。我们只需要做的就是正确加载驱动程序文件以获得数据库连接,实施操作。
在Java中的Class类提供了加载驱动程序的方法,方法格式如下:
public static Class forName(String className) throws ClassNotFoundException
参数className表示驱动类的描述符字符串,例如加载Oracle驱动的语句描述符为:
Class.forName(“com.mysql.jdbc.Driver”);
Mysql数据库的JDBC驱动程序文件可到Mysql官方网站下载。注意加载驱动程序的时候会抛出ClassNotFoundException,SQLException异常。
(2)、定义连接URL,建立数据库连接对象
DriverManager类提供了getConnection()方法可获得指定数据库的连接对象,方法格式如下:
public static Connection getConnection(String url,String userName,String password) throws SQLException
Oracle数据库的url 格式为:
jdbc:mysql://:主机名或IP:3306:<数据库名>
[t1] 下面语句是建立Oracle数据库连接对象的语句:
String url = “jdbc:mysql://127.0.0.1:3306/dept”;
Connection conn = DriverManager.getConnection(url,”root”,”root”);
更多的创建Connection对象的方法,同学们可以查阅Java API文档。
(3)、创建Statement对象
Connection接口中提供了获得Statement对象的方法,方法格式如下:
Statement createStatement() throws SQLException
也可调用重载的createStatement()方法,可指定参数,设置数据库操作结果的相关属性等,更多的创建Statement对象的方法,同学们可以查阅Java API文档。
(4)、执行查询或更新
获取Statement对象后,就可以通过Statement对象的executeQuery()或exeucteUpdate()方法执行查询或者更新操作,有关方法的详细解释,同学们可以查阅Java API文档。
(5)、操作结果集
ResultSet接口提供对结果集进行操作的方法,主要包括:
§ boolean next() throws SQLException:移动结果集操作指针。
§ getXxx(String columnName) throws SQLException:根据传入列的名字获取指定列的值。
§ getXxx(1) throws SQLException:根据传入列的编号获取指定列的值。
SQL类型与Java数据类型不同,表7-1列出了SQL类型与Java数据类型的对应关系。
SQL Type | Java Type |
---|---|
CHAR | String |
VARCHAR | String |
LONGVARCHAR | String |
NUMERIC | java.math.BigDecimal |
DECIMAL | java.math.BigDecimal |
BIT | boolean |
TINYINT | byte |
SMALLINT | short |
INTEGER | int |
BIGINT | long |
REAL | float |
FLOAT | double |
DOUBLE | double |
BINARY | byte[] |
VARBINARY | byte[] |
LONGVARBINARY | byte[] |
DATE | java.sql.Date |
TIME | java.sql.Time |
TIMESTAMP | java.sql.Timestamp |
更多的操作结果集的方法,同学们可以查阅Java API文档。
(6)、释放资源
数据库操作完成后,需要调用ResultSet、Statement、Connection接口中的关闭方法,释放相关资源,关闭顺序如下:首先关闭结果集ResultSet对象,然后关闭Statement对象,最后关闭Connection 对象。
7.2.2 JDBC中主要的类及常用方法
使用JDBC编写访问数据库的应用程序,需要经过加载数据库驱动程序、创建连接、创建Statement对象、发送SQL语句、解析操作结果等步骤,它们由JDBC API中一组类的方法实现。主要的类如下:
(1)、Class类
Class类全称java.lang.Class,Java程序运行时会自动创建程序中的每个类的Class对象,通过Class类的方法,可以得到程序中每个类的信息。Class类方法主要包括:
public static Class forName(String className):该方法根据给定的字符串参数返回相应的Class对象。例:Class.forName(“com.mysql.jdbc.Driver”)的作用是加载Oracle驱动。
public String getName():该方法返回类名,例str.getClass().getName())。
如果想了解更多Class类的其它方法,可以查阅Java API文档。
(2)、DriverManager类
DriverManager类在用户程序和数据库系统之间维护着与数据库驱动程序之间的连接。它实现驱动程序的装载、创建与数据库系统连接的Connection类对象。DriverManager类的方法主要包括:
public static Connection getConnection(String url, String user, String password):根据url、数据库登录的用户名、密码获取一个数据库的连接对象。
(3)、Connection接口
Connection用于管理到指定数据库的连接。
Connection con=DriverManager.getConnection (url, “”, “”);
Connetction类中重要的成员方法包括:
createStatement()方法:创建Statement类的实例。
prepareStatement()方法:创建PreparedStatement类的实例。
close():立即释放此 Connection 对象的数据库和 JDBC 资源,而不是等待它们被自动释放。
(4)、Statement接口
Statement数据库操作类提供执行数据库操作的方法,如更新、查询数据库记录等。
Statement对象的创建方式如下:
Statement stmt=con.createStatement();
Statement类中重要的成员方法包括:
executeQuery()方法:它用来执行一个查询语句,参数是一个String对象,就是一个SELECT语句。它的返回值是ResultSet类的对象,查询结果封装在该对象中。
例:stmt.executeQuery(“select from users where username=‘张三’ and password=‘123’ ”);
executeUpdate()方法:
它用来执行更新操作,参数是一个String对象,即一个更新数据表记录的SQL语句。使用它可以对表中的记录进行修改、插入和删除等操作。例:
stmt.executeUpdate(“INSERT INTO users(username,password) values(‘刘青’, ‘aaa’) ”);
stmt.executeUpdate(“UPDATE users set password=‘bbb’ where username=‘张三’ ”);
stmt.executeUpdate(“DELETE from users where username=‘李四’”);
使用它还可以创建和删除数据表及修改数据表结构。例:
stmt.executeUpdate(“create table users(id int IDENTITY(1,1),username varchar(20))”);
stmt.executeUpdate(“drop table users”);
stmt.executeUpdate(“alter table users add column type char(1)”);
stmt.executeUpdate(“alter table users drop column type”);
close():关闭Statement对象。
Statement接口的好多方法都是重载的,如果想了解更多其它方法,可以查阅Java API文档。
(5)、ResultSet接口
ResultSet结果集类提供对查询结果集进行处理的方法。
例:ResultSet rs=stmt.executeQuery(“select from users”);
ResultSet对象维持着一个指向表格的行的指针,开始时指向表格的起始位置(第一行之前)。ResultSet类常用的方法包括:
next()方法:光标移到下一条记录,返回一个boolean值。
previous()方法:光标移到前一条记录。
getXXX()方法:获取指定类型的字段的值。调用方式getXXX(“字段名”)或getXXX(int i)。i值从1开始表示第一列的字段。
close():关闭ResultSet对象。
例:while(rs.next()){ id=rs.getInt(1); username=rs.getString(“username”); }
ResultSet接口提供的getXxx() 方法如表7-2所示。
表7-2 ResultSet接口的getXxx 方法
Method | Java Technology Type Returned |
---|---|
getASCIIStream | java.io.InputStream |
getBigDecimal | java.math.BigDecimal |
getBinaryStream | java.io.InputStream |
getBoolean | boolean |
getByte | byte |
getBytes | byte[ ] |
getDate | java.sql.Date |
getDouble | double |
getFloat | float |
getInt | int |
getLong | long |
getObject | Object |
getShort | short |
getString | java.lang.String |
getTime | java.sql.Time |
getTimestamp | java.sql.Timestamp |
getUnicodeStream | java.io.InputStream of Unicode characters |
下面以用户表为例,说明使用JDBC对数据库进行操作的方法,用户表结构如表7-3所示:
名称 | 数据类型 | 主键 | 是否为空 | 说明 |
---|---|---|---|---|
ID | number | 是 | 用户编号 | |
NAME | Varchar2(50) | 用户名 | ||
AGE | varchar2(5) | 用户年龄 | ||
BIRTH | date | 用户生日 | ||
PWD | varchar2(20) | 否 | 用户密码 |
【例7-1】使用JDBC查询数据库表t_user的所有数据。
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcMysqlTest {
public static void main(String[] args) throws
ClassNotFoundException,SQLException {
//1.加载jdbc驱动
Class.forName("com.mysql.jdbc.Driver");
//2.定义连接url
String url = "jdbc:mysql://127.0.0.1:3306/neuedu";
//3.获取数据库连接对象
Connection conn = DriverManager.getConnection(url,"root","root");
//4.获得statement对象(用来执行sql语句,并返回结果)
Statement st = conn.createStatement();
//5.执行查询或更新
String sql = "select id,name,age,birth from t_user";
ResultSet rs = st.executeQuery(sql);
//6.处理结果(遍历获取查询出来的所有数据)
while(rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
String age = rs.getString("age");
Date birth = rs.getDate("birth");
System.out.println(id+":"+name+":"+age+":"+birth);
}
//7.关闭连接(释放资源)
rs.close();
st.close();
conn.close();
}
}
程序运行结果如下:
7:zhangsan:age:2015-09-01
8:lisi:24:2015-09-01
9:wangwu:25:2015-09-01
10:wang:23:2015-09-01
以上给大家粗略的介绍了一下JDBC中涉及到的常用相关类和接口,每个类和接口包含的方法介绍的不是十分全面,希望大家在后续的学习过程中,能充分的利用Java API这个工具,不断提升自己的学习能力。
7.2.3JDBC日期时间处理
例7-1中取出来的日期与数据库中存储的有所不同,数据库中birth列包含日期和时间,而上例中只显示了日期,没有时间。这是因为获取日期时使用的是getDate()。对于数据库中不同的时间类型,要分别采用与之相对应的Java包装类来存取:
ü 日期类型用java.sql.Date
ü 时间类型用java.sql.Time
ü 日期/时间类型用java.sql.Timestamp;
ü getTimestamp()可以把年月日时分秒都取出来,getDate()只能取出年月日,getTime()只能取出时分秒。
要把JDBC的日期/时间类型转换为字符串,则可以使用下列方法:
ü Timestamp timeStamp = rs.getTimestamp(“birth”);//通过数据库访问获取到该数据
ü SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); //格式化日期
ü String str = sdf.format(timeStamp);
如何把java.sql.Timestamp转换为java.util.Date?java.sql.Timestamp是java.util.Date的子类,不需要做任何转换直接赋值即可,看下面程序段:
java.sql.Timestamp ts;
java.util.Date utilDate;
utilDate = ts;
反过来,如何把java.util.Date转换为java.sql.Timestamp呢?java.util.Date是java.sql.Timestamp的父类,要这样转换:
java.sql.Timestamp ts;
java.util.Date utilDate;
ts.setTime(utilDate.getTime());
【例7-2】封装打开连接和关闭资源的DBUtil类。
通常,无论是对数据进行查询操作,还是进行增删改操作,都需要打开连接,关闭资源等操作,因此,可以把对把打开连接和关闭连接封装到一个工具类里。本章后面所有例子对数据访问所用连接都是一样的。下面的DBUtil类封装了打开连接和关闭连接方法。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DBUtil {
//该段代码完成加载数据库驱动,整个程序只需要加载一次,所以放在静态块中。
static{
try {
Class.forName("com.mysql.jdbc.Driver");//oracle数据库驱动程序
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//获取数据库连接方法
public static Connection getConnection() throws SQLException{
String url = "jdbc:mysql://127.0.0.1:3306/neuedu";
Connection conn = DriverManager.getConnection(url,"root","root");
return conn;
}
//释放资源
public static void close(Statement st,Connection conn){
try{
if(st != null){
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}finally{
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
public static void close(ResultSet rs, Statement st, Connection conn) {
try {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} finally {
try {
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
}
程序分析:关闭连接会抛出异常,若遇到异常程序会意外终止,因此要处理异常。而为了保证关闭资源语句在出现异常时也会执行到,因此要把它放到finally语句中。另外,因为查询操作才需要关闭ResultSet资源,增删改是不需要关闭ResultSet资源的,因此DBUtil类中对关闭操作进行了重载。
刚才我们定义了数据库通用处理类DBUtil,接下来我们通过一个实例,来看一下这个类应该怎么应用。
【例7-3】使用DBUtil类操作数据库
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
public class UserDao {
//查询t_user表中所有数据
public static void query() throws ClassNotFoundException, SQLException{
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try{
conn = DBUtil.getConnection();//直接调用DBUtil类的获取数据库连接方法
String sql = "select id,name,age,birth from t_user";
st = conn.createStatement();
rs = st.executeQuery(sql);
while(rs.next()){
String id = rs.getString(1);
String name = rs.getString(2);
int age = rs.getInt(3);
Timestamp ts= rs.getTimestamp("birth");
//对ts进行格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String birth = sdf.format(ts);
System.out.println(id + " $ " + name + " $ " + age + " $ " + birth);
}
}finally{
DBUtil.close(rs, st, conn); //调用DBUtil方法释放资源。
}
}
//在main()方法中调用查询操作。
public static void main(String[] args) throws ClassNotFoundException, SQLException {
query();
}
}
运行结果分析:
调用query()方法输出结果:
7 $ zhangsan $ 23 $ 2015年09月01日 15:15:06
8 $ lisi $ 24 $ 2015年09月01日 15:15:23
9 $ wangwu $ 25 $ 2015年09月01日 15:15:52
10 $ hello1 $ 500 $ 2015年09月01日 15:16:03
7.2.4 SQL注入问题
在使用Statement对象查询数据库时,由于定义的SQL语句是拼接的,有可能出现SQL注入问题。所谓SQL注入,就是通过把SQL命令插入到查询字符串,最终达到欺骗服务器执行恶意的SQL命令。接下来我们通过一段代码来演示一下SQL注入案例。
【例7-4】登录功能SQL注入示例。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class SqlInject {
public static void login (String name,String pwd) throws SQLException{
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try{
conn = DBUtil.getConnection();
st = conn.createStatement();
String sql = "SELECT * FROM t_user WHERE NAME='"+name+"' AND PWD='"+pwd+"'";
rs = st.executeQuery(sql);
if(rs.next()){
System.out.println("登录成功..");
}else{
System.out.println("登录失败..");
}
}finally{
DBUtils.close(rs, st, conn);
}
}
public static void main(String[] args) throws SQLException {
login("123123", "sadfsdf or 1=1");//注入SQL
}
}
程序分析:上例中若传入的用户名为sadfsdf or 1=1,则不管用户名和密码是否正确,都能成功登录,因为1=1永远为真,和其它条件进行or操作,也永远为真,所以不管用户名和密码是否正确,都能成功登录,这就是SQL 注入问题。
7.3PreparedStatement
PreparedStatement对象表示预编译的 SQL 语句的对象,为解决Statement静态拼接所产生的SQL 注入问题,引入了PreparedStatement接口。PreparedStatement接口是Statement接口的子接口,允许使用不同的参数多次执行同样的SQL语句。Connection接口提供创建PreparedStatement对象的方法,可指定SQL语句:
PreparedStatement prepareStatement(String sql) throws SQLException
PreparedStatement对象继承了Statement,但PreparedStatement语句中包含了警告预编译的SQL语句,因此可以获得更高的执行效率。虽然使用Statement可以对数据库进行操作,但它只适用于简单的SQL语句。如果需要执行带参数的SQL语句时,我们必须利用PreparedStatement类对象。PreparedStatement对象用于执行带或不带输入参数的预编译的SQL语句,语句中可以包含多个用问号”?”代表的字段,在程序中可以利用setXxx()方法设置该字段的内容,从而增强了程序设计的动态性。
例如,在案例中要查询编号为1的人员信息,可用以下代码段:
ps=con. PreparedStatement(“select id,name from person where id=?”);
ps.setInt(1,1);
接着当我们需查询编号为2的人员信息时,我们仅需以下代码:
ps.setInt(1,2);
PreparedStatement同Statement对象一样提供了很多基本的数据库操作方法,下面列出了执行SQL命令的3种方法。
(1)ResultSet executeQuery():可以执行SQL查询并获取ResultSet对象
(2)int executeUpdate():可以执行Update /Insert/Delete操作,返回值是执行该操作所影响的行数。
(3)boolean execute():这是一个最为一般的执行方法,可以执行任意SQL语句,然后获得一个布尔值,表示是否返回ResultSet。
【例7-5】使用PreparedStatement解决例7-4中登录功能的SQL注入问题。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class SqlInject {
public static void login(String name, String PWD) throws SQLException{
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
conn = DBUtil.getConnection();
String sql = "SELECT * FROM t_user WHERE NAME=? AND PWD=?";
ps = conn.prepareStatement(sql);
//设置参数
ps.setString(1, id);
ps.setString(2, name);
rs = ps.executeQuery();
if(rs.next()){
System.out.println("登录成功..");
}else{
System.out.println("登录失败..");
}
}finally{
DBUtil.close(rs, ps, conn);
}
}
public static void main(String[] args) throws SQLException {
login("123123", "sadfsdf' or 1=1 or ''='");//解决注入SQL
}
}
程序分析:采用以上方式,登录不能成功,解决了sql注入问题。 PreparedStatement的参数化的查询可以阻止大部分的SQL注入。在使用参数化查询的情况下,数据库系统不会将参数的内容视为SQL指令的一部分来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。
PreparedStatement接口setXxx()方法如表7-3所示。
表7-3 PreparedStatement接口setXxx方法
Method | SQL Type |
---|---|
setASCIIStream | LONGVARCHAR produced by an ASCII stream |
setBigDecimal | NUMERIC |
setBinaryStream | LONGVARBINARY |
setBoolean | BIT |
setByte | TINYINT |
setBytes | VARBINARY or LONGVARBINARY (depending on the size relative to the limits on VARBINARY) |
setDate | DATE |
setDouble | DOUBLE |
setFloat | FLOAT |
setInt | INTEGER |
setLong | BIGINT |
setNull | NULL |
setObject | The given object that is converted to the target SQL type before being sent |
setShort | SMALLINT |
setString | VARCHAR or LONGVARCHAR (depending on the size relative to the driver’s limits on VARCHAR) |
setTime | TIME |
setTimestamp | TIMESTAMP |
【例7-6】利用PreparedStatement实现对用户表的增删改查操作。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
public class UserDaoPreparedStatement {
//插入操作
public static void insert(String name, int age, Date birth)
throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DBUtil.getConnection();
String sql = "insert into t_user values(seq_user.nextval,?,?,?)";
ps = conn.prepareStatement(sql);
// 设置参数,有几个?就需要设置几个参数值
ps.setString(1, name);
ps.setInt(2, age);
ps.setTimestamp(3, new Timestamp(birth.getTime()));
int result = ps.executeUpdate();
if (result > 0) {
System.out.println("insert 成功...");
} else {
System.out.println("insert 失败...");
}
} finally {
DBUtil.close(ps, conn);
}
}
public static void main(String[] args) throws SQLException {
insert("hello", 234, new Date());
}
}
本例讲解了插入操作,关于删除及修改操作的详细代码请参考本书配套光盘。
实际项目中经常使用MVC分层思想实现对数据库的操作,MVC模式中的Model(模型)是应用程序中用于处理应用程序业务逻辑的部分。View(视图)是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。Controller(控制器)是应用程序中协调Model组件和View组件的部分,通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
【例7-5】使用MVC思想进行DAO设计与框架搭建。
首先定义User类,把用户表封装成实体。
import java.util.Date;
public class User {
private String id;
private String name;
private int age;
private Date birth;
public User(){
}
public User(String id, String name, int age, Date birth) {
super();
this.id = id;
this.name = name;
this.age = age;
this.birth = birth;
}
publicint getAge() {
returnage;
}
publicvoid setAge(int age) {
this.age = age;
}
public String getId() {
returnid;
}
publicvoid setId(String id) {
this.id = id;
}
public String getName() {
returnname;
}
publicvoid setName(String name) {
this.name = name;
}
}
对于数据库的每一张表,都要定义一个数据访问类,先定义访问用户表的接口UserDao。
import java.lang.Exception;
import java.util.List;
public interface UserDao {
public boolean insert(User user) throws Exception;// 添加用户
public boolean update(User user) throws Exception;//更新用户信息
public boolean delete(String id) throws Exception;// 根据用户ID删除用户
public List<User> query() throws Exception;// 查询当前所有用户
public User queryById(String id)throws Exception;// 根据用户ID查询当前用户信息
}
之所以定义一个数据访问的接口,是因为面向接口编程,能够起到封装和解耦合的作用,而面向具体类编程,当类中的方法改变时,调用它的类也要相应做出变更,所以面向对象的一条核心编程思想是更多的采用面向接口编程。下面是访问数据库接口的实现类。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class UserDaoJdbcImpl implements UserDao{
public List<User> query() throws Exception {
List<User> users = new ArrayList<User>();
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
conn = DBUtil.getConnection();
String sql = "select id,name,age,birth from t_user";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
//遍历结果集,并将数据封装到list中
User u = null;
while(rs.next()){
String id = rs.getString(1);
String name = rs.getString(2);
int age = rs.getInt(3);
Date birth = rs.getDate(4);
u = new User(id,name,age,birth);
users.add(u);
}
}finally{
DBUtil.close(rs,ps, conn);
}
return users;
}
public User queryById(String id) throws Exception {
User user = null;;
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
conn = DBUtils.getConnection();
String sql = "select id,name,age,birth from t_user where id=?";
ps = conn.prepareStatement(sql);
ps.setString(1, id);
rs = ps.executeQuery();
//遍历结果集,并将数据封装到list中
if(rs.next()){
String name = rs.getString(2);
int age = rs.getInt(3);
Date birth = rs.getDate(4);
user = new User(id,name,age,birth);
}
}finally{
DBUtil.close(rs,ps, conn);
}
return user;
}
}
在测试类中,调用接口中的方法实现数据库的查询操作。
import java.util.Date;
import java.util.List;
import User;
import UserDao;
import UserDaoJdbcImpl;
public class Test {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("oop");
user.setAge(23);
user.setBirth(new Date());
UserDao dao = new UserDaoJdbcImpl();
List<User> users = dao.query();
for (User u : users) {
System.out.println(u.getId() + ":" + u.getName() + ":" + u.getAge()
+ ":" + u.getBirth());
}
}
}
在使用PreparedStatement对象执行SQL命令时,命令被数据库进行解析和编译,然后被放到命令缓冲区。然后,每当执行同一个 PreparedStatement对象时,它就会被再解析一次,但不会被再次编译。在缓冲区中可以发现预编译的命令,并且可以重新使用。在有大量用户的企业级应用软件中,经常会重复执行相同的SQL命令,使用PreparedStatement对象带来的编译次数的减少能够提高数据库的总体性能。如果不是在客户端创建、预备、执行PreparedStatement任务需要的时间长于Statement任务,建议在除动态SQL命令之外的所有情况下使用PreparedStatement对象。相对于Statement,PreparedStatement的优点如下:
- 可动态设置参数。
- 增加了预编译功能。
- 提高执行速度。
7.4 用JDBC连接不同的数据库
上面我们讲解了使用JDBC连接Oracle数据库的方法,因为JDBC是一套数据库连接的标准,所以连接其它关系型数据库也都大同小异,两点区别在于:
1、数据库驱动路径不同
2、连接数据库的url不同
如下是各种数据库的JDBC连接方式
(1)MySql数据库
Class.forName(“com.mysql.jdbc.Driver”);
String url = “jdbc:mysql://localhost:3306/test”;//取得连接的url
String userName = “root”;//使用能访问MySQL数据库的用户名root
String password = “mysql”; //使用口令
Connectioncon=DriverManager.getConnection(url,userName,password);
(2) DB2数据库
Class.forName(“com.ibm.jdbc.app.DB2Driver”);
String url=”jdbc:db2://localhost:5000:sample”;// sample为数据库名
String user=”admin”;
String password=””;
Connection con=DriverManager.getConnection(url,user,password);
(3)Sybase数据库
Class.forName(“com.sybase.jdbc.SybDriver”);
String url=”jdbc:Sybase:Tds:localhost:5007//sample”;// sample为数据库名
Properties sysProps=System.getProperties();
sysProps.put(“user”,”userid”);
sysProps.put(“password”,”user_password”);
Connection con=DriverManager.getConnection(url, sysProps);
(4)SQLServer数据库
Class.forName(“Com.microsoft.jdbc.sqlserver.SQLServerDriver”);
String url=”Jdbc:Microsoft:sqlserver://localhost:1433//sample”;
//sample为数据库名
String user=”admin”;
String password=”admin”;
Connection con=DriverManager.getConnection(url,user,password);7.5 事务处理
7.5.1事务的概念
事务是保持数据一致性的一种数据库机制,通常大多数应用系统中,除了查询操作(SELECT)不需要对事务进行控制之外,其它数据操作(INSERT、UPDATE、DELETE)都会涉及到事务的操作,在JDBC中,事务主要是由数据库连接对象Connection的相关方法来控制,涉及到主要方法包括:
1.setAutoCommit(boolean autoCommit):将此连接的自动提交模式设置为给定状态。
2.commit():使所有上一次提交/回滚后进行的更改成为持久更改,并释放此 Connection 对象当前持有的所有数据库锁。
3.rollback():取消在当前事务中进行的所有更改,并释放此 Connection 对象当前持有的所有数据库锁。如果要取消事务中的操作,则可以使用ROLLBACK命令。执行该命令后,事务中的所有操作都被取消,数据库恢复到事务开始之前的状态,同时事务所占用的系统资源和数据库资源被释放。
如果只想取消事务中的部分操作,而不是取消全部操作,则可以在事务内部设置保存点,将一个大的事务划分为若干个组成部分,这样就可以将事务回滚到指定的保存点。
connection.rollback();//回滚事务。
SavePoint sp = connection.setSavepoint(); //设置保存点
connection.rollerbak(sp); //回滚到保存点
connection.commit();
可以使用SAVEPOINT语句设置保存点,例如,一个事务中包含3个插入操作、一个更新操作和2个保存点,语句为:
SQL>INSERT INTO departments VALUES(400,’ACCOUNTING’,100,1500);
SQL>INSERT INTO departments VALUES(410,’SALES’,120,1600);
SQL>SAVEPOINT A;
SQL>UPDATE departments SET location_id=1600 where department_id=400;
SQL>SAVEPOINT B;
SQL>INSERT INTO departments VALUES(420,’RESEARCH’,130,1700);
在该事务提交之前,可以使用ROLLBACK命令全部或部分回滚事务中的操作。语句为:
SQL>ROLLBACK TO B;(回滚最后一个INSERT操作)
SQL>ROLLBACK TO A; (回滚后面的INSERT操作和UPDATE操作)
SQL>ROLLBACK; (回滚全部操作)
事务回滚的过程示意图如图7-4所示。
4. setSavepoint():在当前事务中创建一个未命名的保存点 (savepoint),并返回表示它的新 Savepoint 对象。
5.setSavepoint(String name) :在当前事务中创建一个具有给定名称的保存点,并返回表示它的新Savepoint 对象。
上述前三个方法的使用频率特别高,通常应用系统中一个业务操作的实现步骤为:
- 在创建好数据库连接对象Connect之后,调用setAutoCommit(false)方法,把自动提交模式设置为假,这样就避免了数据库执行自动提交,而把是否提交的主动权交到程序员手中。
- 按照用户的操作顺序执行相应的SQL语句。
- 在执行每一个SQL语句过程中,如果出现非预期的结果,则调用rollback()方法,执行回滚操作。
- 所有SQL语句执行完成后,通常调用commit()方法执行提交操作,当然这时也可以执行回滚操作。
接下来我们通过一个实例来看一下JDBC中事务的操作。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;
public class UserDaoBak {
//转账的方法
public void transfer() throws Exception{
//将zhangshan的money -100 lishi的money+100
Connection conn = null;
PreparedStatement ps = null;
try{
conn = DBUtil.getConnection();
//将zhangshan的money -100
String sql1 = "update t_User set money=money-100 where id=1";
ps = conn.prepareStatement(sql1);
ps.executeUpdate();
Savepoint sp = conn.setSavepoint();
//将lishi的money +100
String sql2 = "update t_User set money=money+100 where id=2";
ps = conn.prepareStatement(sql2);
//模拟一个异常
if(1 == 1)
throw new RuntimeException("一个模拟的异常....");
ps.executeUpdate();
//提交事务
conn.commit();
System.out.println("转账成功");
}catch(Exception e){
e.print();
}finally{
DBUtil.close(ps, conn);
}
}
}
//如下为测试方法
public class Test {
public static void main(String[] args) throws Exception {
UserDaoBak userDaoBak=new UserDaoBak();
userDaoBak.transfer();
}
}
程序分析:上例中实现把张三账户的100块钱转给李四,若不出现异常,则数据库中张三的钱数为2900,李四的钱数为2100。如图7-5所示。
图7-5 转账成功数据库数据正确更新
假如上例中把抛出异常的语句去掉再次运行,会抛出运行时异常,而数据库中数据如图7-6所示。
在张三转出之后,李四转入之前之间抛出个异常,则会出现张三的钱数减少,而李四的钱数却没有增加,转出的100块钱丢失了。出现了数据不一致问题,若要解决上述问题,需要用到JDBC的事务处理。
【例7-7】使用事务解决【例7-6】中的数据不一致问题。
public class UserDaoBak {
//转账的方法
public void transfer() throws Exception{
//将zhangshan的money -100 lishi的money+100
Connection conn = null;
PreparedStatement ps = null;
try{
conn = DBUtils.getConnection();
//关闭conn的自动提交
conn.setAutoCommit(false);
//将zhangshan的money -100
String sql1 = "update t_User set money=money-100 where id=1";
ps = conn.prepareStatement(sql1);
ps.executeUpdate();
//Savepointsp = conn.setSavepoint();
//将lishi的money +100
String sql2 = "update t_User set money=money+100 where id=2";
ps = conn.prepareStatement(sql2);
//模拟一个异常
if(1 == 1)
throw new RuntimeException("一个模拟的异常....");
ps.executeUpdate();
//提交事务
conn.commit();
//conn.setAutoCommit(true);
System.out.println("转账成功");
}catch(Exception e){
if(conn != null){
//如果执行过程中出异常,则回滚事务
conn.rollback();
conn.setAutoCommit(true);
}
throw e;
}finally{
DBUtil.close(ps, conn);
}
}
}
程序分析:上例中,在转账之前,把事务的自动提交功能关闭,默认情况下JDBC是自动提交事务的。若抛出异常,则事务回滚,李四账号没收到钱,则张三账户的钱也不会减少。这样就不会出现数据不一致的情况。
7.6 JDBC编程续注意事项
本章主要讲述了JDBC中的常用类、接口以及方法的使用,为了更好的给读者展示效果,实例代码基本都写到数据访问对象DAO中,例如本章中大量出现的UserDAO类。
在实际项目的编写过程中,通常都是按照Model-View-Control(模型层-视图层-控制层)架构模式来组织代码,通常来说,数据访问对象DAO是属于模型层的一种对象,专门用来编写对数据库中数据的增删改查操作,而对数据库连接对象以及事务的管理,通常不会放到数据访问对象DAO中来处理,否则,如果每个DAO的方法都来创建一个Connection连接对象,就会过多的占用数据连接资源,同时也就无法进行事务控制了。
7.7 课后作业
一、选择题()
1.使用下面的Connection 的哪个方法可以建立一个PreparedStatement接口对象( )
A) createPrepareStatement()
B) prepareStatement()
C) createPreparedStatement()
D) preparedStatement()
2. 在JDBC中可以调用数据库的存储过程的接口是 ( )
A) Statement
B) PreparedStatement
C )CallableStatement
D) PrepareStatement
3. 下面的描述正确的是 ( )
A) PreparedStatement继承自Statement
B) Statement继承自PreparedStatement
C) ResultSet继承自Statement
D) CallableStatement继承自PreparedStatement
4. 下面的描述错误的是 ( )
A) Statement的executeQuery()方法会返回一个结果集
B) Statement的executeUpdate()方法会返回是否更新成功的boolean值
C) 使用ResultSet中的getString()可以获得一个对应于数据库中char类型的值
D) ResultSet中的next()方法会使结果集中的下一行成为当前行
5. 如果数据库中某个字段为numberic型,可以通过结果集中的哪个方法获取 ( )
A) getNumberic()
B) getDouble()
C) setNumberic()
D) setDouble()
6.下面用来代表数据库连接的接口是( )。
A) Statement
B) ResultSet
C) ResultSetMetaData
D) PreparedStatement
E) Connection
7.下面用来代表数据集的接口是( )。
A) Statement
B) ResultSet
C) ResultSetMetaData
D) PreparedStatement
E) Connection
二、简答题
- 什么是JDBC,在什么时候会用到它?
- 简述JDBC应用程序的基本步骤。
- 简述你对Statement,PreparedStatement,CallableStatement的理解。
- 简述JDBC提供的连接数据库的几种方法?
- 编写用JDBC编写能实现数据库连接和断开的程序代码。
- 简述Class.forName()的作用。
- JDBC的DriverManager是用来做什么的?
- 相对于Statement,PreparedStatement的优点是什么?
- execute(),executeQuery(),executeUpdate()三个方法的主要区别是什么?
- JDBC API提供的类或接口主要有哪些?
三、编程题
1、写一个JDBC类,包括3个方法,分别实现对用户表的如下操作。
User表: | ||||
---|---|---|---|---|
字段名称 | 说明 | 数据类型 | 约束 | 备注 |
name | 用户名 | varchar2(10) | 主键 | |
pwd | 密码 | varchar2(6) | 不允许空 | |
邮箱 | varchar2(64) | |||
birthday | 生日 | date |
(1)将表中插入如下数据:
name | pwd | birthday | |
---|---|---|---|
张三 | 888888 | zhangsan@126.com | 1986-10-11 |
李四 | 999999 | lisi@126.com | 1988-10-23 |
王五 | 777777 | wangwu@126.com | 1990-9-11 |