02-JDBC

unit 02-JDBC

JDBC概述

什么是JDBC?为什么要学习JDBC?

JDBC(Java Database Connectivity) Java数据库连接

其实就是利用Java语言/程序连接并访问数据库的一门技术

之前我们可以通过CMD或者navicat等工具连接数据库

但在企业开发中,更多的是通过程序(Java程序)连接并访问数据库,通过Java程序访问数据库,就需要用到JDBC这门技术。

5fa0f308f5ff06beac3c7f18614c9ff3

5fa0f308f5ff06beac3c7f18614c9ff3

如何通过JDBC程序访问数据库?

1、提出需求:

创建一个 jt_db 数据库,在库中创建一个account表,并插入三条记录,然后利用Java程序查询出account表中所有的记录,并将查询的结果打印在控制台上。

2、开发步骤:

(1)准备数据, 创建jt_db库, 创建account表

1
2
3
4
5
6
7
8
9
10
11
drop database if exists jt_db;
create database jt_db charset utf8;
use jt_db;
create table account(
id int primary key auto_increment,
name varchar(50),
money double
);
insert into account values(null, 'tom', 1000);
insert into account values(null, 'andy', 1000);
insert into account values(null, 'tony', 1000);

如果已经执行过课前资料中的”SQL脚本文件”,此步骤可以跳过。

(2)创建JAVA工程:

image-20200318153229937

image-20200318153229937

(3)导入jar包——mysql驱动包:

image-20200318153627820

image-20200318153627820

6ba3042ef2947f314836128a5c19e2c2

6ba3042ef2947f314836128a5c19e2c2

(4)创建类并实现JDBC程序(六个步骤)

dccd4a8163b4835b74e77f945d106683

dccd4a8163b4835b74e77f945d106683

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws Exception {
//1.注册数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取数据库连接
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jt_db?characterEncoding=utf-8",
"root", "root");
//3.获取传输器
Statement stat = conn.createStatement();
//4.发送SQL到服务器执行并返回执行结果
String sql = "select * from account";
ResultSet rs = stat.executeQuery( sql );
//5.处理结果
while( rs.next() ) {
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
System.out.println(id+" : "+name+" : "+money);
}
//6.释放资源
rs.close();
stat.close();
conn.close();
System.out.println("TestJdbc.main()....");
}

3、执行结果:

a37ab7234bb1beb988f4b9c01c8cee2f

a37ab7234bb1beb988f4b9c01c8cee2f

JDBC API总结

1、注册数据库驱动

Class.forName(“com.mysql.jdbc.Driver”);

所谓的注册驱动,就是让JDBC程序加载mysql驱动程序,并管理驱动

驱动程序实现了JDBC API定义的接口以及和数据库服务器交互的功能,加载驱动是为了方便使用这些功能。

2、获取连接之数据库URL

1
2
3
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jt_db?characterEncoding=utf-8",
"root", "root" );

DriverManager.getConnection() 用于获取数据连接,返回的Connection连接对象是JDBC程序连接数据库至关重要的一个对象。

参数2参数3分别是所连接数据库的用户名和密码。

参数1:”jdbc:mysql://localhost:3306/jt_db” 是连接数据库的URL,用于指定访问哪一个位置上的数据库服务器及服务器中的哪一个数据库,其写法为:

7edb0faa5fac5f29cd19411909c708cb

7edb0faa5fac5f29cd19411909c708cb

当连接本地数据库,并且端口为3306,可以简写为如下形式:

1
jdbc:mysql:///jt_db

3、Statement传输器对象

1
2
Statement stat = conn.createStatement();
该方法返回用于向数据库服务器发送sql语句的Statement传输器对象

该对象上提供了发送sql的方法:

1
2
3
4
executeQuery(String sql) --
用于向数据库发送查询类型的sql语句,返回一个ResultSet对象中
executeUpdate(String sql) --
用于向数据库发送更新(增加、删除、修改)类型的sql语句,返回一个int值,表示影响的记录行数

4、ResultSet结果集对象

ResultSet对象用于封装sql语句查询的结果,也是一个非常重要的对象。该对象上提供了遍历数据及获取数据的方法。

(1)遍历数据行的方法

next() – 使指向数据行的箭头向下移动一行,并返回一个布尔类型的结果,true表示箭头指向了一行数据,false表示箭头没有指向任何数据(后面也没有数据了)

(2)获取数据的方法

1
2
3
4
5
6
7
8
getInt(int columnIndex)
getInt(String columnLable)
getString(int columnIndex)
getString(String columnLable)
getDouble(int columnIndex)
getDouble(String columnLable)
getObject(int columnIndex)
getObject(String columnLable)

5、释放资源

1
2
3
rs.close();
stat.close();
conn.close();

此处释放资源必须按照一定的顺序释放,越晚获取的越先关闭。所以先关闭
rs对象,再关闭stat对象,最后关闭conn对象。

另,为了避免上面的程序抛出异常,释放资源的代码不会执行,应该把释放资源的代码放在finally块中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
try{
...
}catch(Exception e){
...
}finally{
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
rs = null;
}
}
if (stat != null) {
try {
stat.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
stat = null;
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
conn = null;
}
}
}

增删改查

1、新增:往account表中添加一个名称为john、money为3500的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 1、新增:往account表中添加一个名称为john、money为3500的记录 */
@Test
public void testInsert() {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//注册驱动并获取连接
conn = JdbcUtil.getConn();
//获取传输器
stat = conn.createStatement();
//发送sql语句到服务器执行,并返回执行结果
String sql = "insert into account values(null, 'john', 3500)";
int rows = stat.executeUpdate( sql );
//处理结果
System.out.println( "影响行数: "+rows );
} catch (Exception e) {
e.printStackTrace();
} finally {
//通过JdbcUtil工具类中的close方法释放资源
JdbcUtil.close(conn, stat, rs);
}
}

2、修改:将account表中名称为john的记录,money修改为1500

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 2、修改:将account表中名称为john的记录,money修改为1500 */
@Test
public void testUpdate() {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//注册驱动并获取连接
conn = JdbcUtil.getConn();
//获取传输器
stat = conn.createStatement();
//发送sql语句到服务器执行,并返回执行结果
String sql = "update account set money=1500 where name='john'";
int rows = stat.executeUpdate( sql );
//处理结果
System.out.println( "影响行数: "+rows );
} catch (Exception e) {
e.printStackTrace();
} finally {
//通过JdbcUtil工具类中的close方法释放资源
JdbcUtil.close(conn, stat, rs);
}
}

3、查询:查询account表中名称为john的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 3、查询:查询account表中id为1的记录 */
@Test
public void testFindById() {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//注册驱动并获取连接
conn = JdbcUtil.getConn();
//获取传输器
stat = conn.createStatement();
//执行sql语句,返回执行结果
String sql = "select * from account where id=1";
rs = stat.executeQuery( sql );
//处理结果
if( rs.next() ) {
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
System.out.println( id+" : "+name+" : "+money);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JdbcUtil.close(conn, stat, rs);
}
}

4、删除:删除account表中名称为john的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 4、删除:删除account表中名称为john的记录 */
@Test
public void testDelete() {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//注册驱动并获取连接
conn = JdbcUtil.getConn();
//获取传输器
stat = conn.createStatement();
//发送sql语句到服务器执行,并返回执行结果
String sql = "delete from account where name='john'";
int rows = stat.executeUpdate( sql );
//处理结果
System.out.println( "影响行数: "+rows );
} catch (Exception e) {
e.printStackTrace();
} finally {
//通过JdbcUtil工具类中的close方法释放资源
JdbcUtil.close(conn, stat, rs);
}
}

内容补充:使用单元测试测试上面的方法

单元测试:不用创建新的类,也不用提供main函数,也不用创建类的实例,就可以直接执行一个方法。能使用单元测试测试的方法需要满足以下几点:

1
2
3
4
(1)方法必须是公共的
(2)方法必须是非静态的
(3)方法必须是无返回值的
(4)方法必须是无参数的

PreparedStatement

在上面的增删改查的操作中,使用的是Statement传输器对象,而在开发中我们用的更多的传输器对象是PreparedStatement对象,PreparedStatement是Statement的子接口,比Statement更加安全,并且能够提高程序执行的效率。

Statement 父对象

PreparedStatement 子对象

模拟用户登录案例

(1)准备数据

1
2
3
4
5
6
7
8
use jt_db;
create table user(
id int primary key auto_increment,
username varchar(50),
password varchar(50)
);
insert into user values(null,'张三','123');
insert into user values(null,'李四','234');

(2)创建LoginUser 类,提供 main 方法 和 login 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void main(String[] args) {
/* 1、提示用户登录,提示用户输入用户名并接收用户名
* 2、提示用户输入密码并接收密码
* 3、根据用户名和密码查询用户信息
*/
// 1、提示用户登录,提示用户输入用户名并接收用户名
Scanner sc = new Scanner(System.in);
System.out.println( "请登录:" );
System.out.println( "请输入用户名:" );
String user = sc.nextLine();

// 2、提示用户输入密码并接收密码
System.out.println( "请输入密码:" );
String pwd = sc.nextLine();

// 3、根据用户名和密码查询用户信息
login( user, pwd );
}
/**
* 根据用户名和密码查询用户信息
* @param user 用户名
* @param pwd 密码
*/
private static void login(String user, String pwd) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//1.注册驱动并获取连接
conn = JdbcUtil.getConn();
//2.获取传输器,执行sql并返回执行结果
stat = conn.createStatement();
String sql = "select * from user where username='"+user+"' and password='"+pwd+"'";
rs = stat.executeQuery(sql);
System.out.println( sql );
//3.处理结果
if( rs.next() ) { //有数据 -- 用户名密码都正确
System.out.println("恭喜您登录成功!");
}else { //没数据 -- 用户名或密码不正确
System.out.println("登录失败, 用户名或密码不正确!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.释放资源
JdbcUtil.close(conn, stat, rs);
}
}

执行时,输入:

1
2
3
4
5
6
7
请登录:
请输入用户名:
张飞'#'
请输入密码:

select * from user where username='张飞'#'' and password=''
恭喜您登录成功了!

或输入

1
2
3
4
5
6
7
请登录:
请输入用户名:
张飞' or '1=1
请输入密码:

select * from user where username='张飞' or '1=1' and password=''
恭喜您登录成功了!

或输入

1
2
3
4
5
6
7
请登录:
请输入用户名:

请输入密码:
' or '2=2
select * from user where username='' and password='' or '2=2'
恭喜您登录成功了!

SQL注入攻击

通过上面的案例,我们发现在执行时,不输入密码只输入用户名也可以登陆成功。这就是SQL注入攻击。

SQL注入攻击产生的原因: 由于后台执行的SQL语句是拼接而来的:

1
select * from user where username='"+user+"' and password='"+pwd+"'

其中的参数是用户提交过来的,如果用户在提交参数时,在参数中掺杂了一些SQL关键字(比如or)或者特殊符号(#、– 、’ 等),就可能会导致SQL语句语义的变化,从而执行一些意外的操作(用户名或密码不正确也能登录成功)!

防止SQL注入攻击

如何防止SQL注入攻击?

(1)使用正则表达式对用户提交的参数进行校验。如果参数中有(# – ‘ or等)这些符号就直接结束程序,通知用户输入的参数不合法

(2)使用PreparedStatement对象来替代Statement对象。

下面通过第二种方式解决SQL注入攻击:添加loginByPreparedSatement方法,在方法中,使用PreparedStatement来代替Statement作为传输器对象使用,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 根据用户名和密码查询用户信息
* @param user 用户名
* @param pwd 密码
*/
private static void login(String user, String pwd) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1.注册驱动并获取连接
conn = JdbcUtil.getConn();
//2.获取传输器,执行sql并返回执行结果
String sql = "select * from user where username=? and password=?";
ps = conn.prepareStatement( sql );
//设置SQL语句中的参数
ps.setString( 1 , user );
ps.setString( 2 , pwd );
//执行SQL语句
rs = ps.executeQuery();//这里不要再传输SQL语句

//3.处理结果
if( rs.next() ) { //有数据 -- 用户名密码都正确
System.out.println("恭喜您登录成功!");
}else { //没数据 -- 用户名或密码不正确
System.out.println("登录失败, 用户名或密码不正确!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4.释放资源
JdbcUtil.close(conn, ps, rs);
}
}

再次执行程序,按照上面的操作登录。此时,已经成功的防止了SQL注入攻击问题了。

PreparedStatement对象是如何防止SQL注入攻击的:

使用PreparedStatement对象是先将SQL语句的骨架发送给服务器编译,编译之后SQL语句的骨架和语义就不会再被改变了,再将SQL语句中的参数发送给服务器,即使参数中再包含SQL关键字或者特殊符号,也不会导致SQL的骨架或语义被改变,只会被当作普通的文本来处理!


使用PreparedStatement对象可以防止SQL注入攻击

而且通过方法设置参数更加的方便且不易出错!

还可以从某些方面提高程序执行的效率!

单元测试补充:

加了@Test注解的方法,可以通过单元测试(junit)框架测试该方法。底层会创建该方法所在类的实例,通过实例调用该方法

1
2
3
4
@Test
public void testInsert() {
System.out.println("TestPreparedStatement.testInsert()");
}

能够使用@Test单元测试测试的方法必须满足如下几个条件:

1
2
3
4
(1)方法必须是公共的
(2)方法必须是非静态的
(3)方法必须是无返回值的
(4)方法必须是无参数的

数据库连接池

什么是连接池

池:指内存中的一片空间(容器,比如数组、集合)

连接池:就是将连接存放在容器中,供整个程序共享,可以实现连接的复用,减少连接创建和关闭的次数,从而提高程序执行的效率!

d30da01edb7ccc007425c91328373339

d30da01edb7ccc007425c91328373339

为什么要使用连接池

1、传统方式操作数据库

ed6ceb2908a9c2a66c3336a651056ce0

ed6ceb2908a9c2a66c3336a651056ce0

1
2
3
Connection conn = DriverManager.getConnection( url, user, pwd ); 
//创建连接对象
conn.close(); //关闭连接, 销毁连接

在传统方式中,每次用户需要连接访问数据库时,都是创建一个连接对象,基于这个连接对象访问数据库,用完连接后,会将连接关闭(conn.close)。

由于每次创建连接和关闭连接非常的耗时间而且耗资源因此效率低下。

2、使用连接池操作数据库

b7566731966b48e3d918bbd205d30030

b7566731966b48e3d918bbd205d30030

可以在程序一启动时,就创建一批连接放在一个连接池中(容器),当用户需要连接时,就从连接池中获取一个连接对象,用完连接后,不要关闭,而是将连接再还回连接池中,这样一来,用来用去都是池中的这一批连接,实现了连接的服用,减少了连接创建和关闭的次数,从而提高了程序执行的效率!

如何使用C3P0连接池

dbcp/c3p0/druid

使用C3P0连接池开发步骤:

1、导入开发包

d9837a8227847f3e22aa28a9e5bf6aa5

d9837a8227847f3e22aa28a9e5bf6aa5

2、创建数据库连接池(对象)

1
ComboPooledDataSource cpds = new ComboPooledDataSource();

3、设置连接数据库的基本信息

(1)方式一:(不推荐) 直接将参数通过 pool.setXxx方法设置给c3p0程序

这种方式直接将参数写死在了程序中,后期一旦参数发生变化,就要修改程序,要重新编译项目、重新发布项目,非常麻烦。

1
2
3
4
5
//设置连接数据库的基本信息
pool.setDriverClass( "com.mysql.jdbc.Driver" );
pool.setJdbcUrl( "jdbc:mysql:///jt_db?characterEncoding=utf-8" );
pool.setUser( "root" );
pool.setPassword( "root" );

(2)方式二:将连接参数提取到properties文件中(推荐)

在类目录下(开发时可以放在src或者类似的源码目录下),添加一个c3p0.properties文件,配置内容如下:

1
2
3
4
c3p0.driverClass=com.mysql.jdbc.Driver
c3p0.jdbcUrl=jdbc:mysql:///jt_db?characterEncoding=utf-8
c3p0.user=root
c3p0.password=root

这种方式由于是c3p0到指定的位置下寻找指定名称的properties文件,所以文件的位置必须是放在src或其他源码根目录下,文件名必须是c3p0.properties。

(3)方式三:(推荐)

在类目录下(开发时可以放在src或者类似的源码目录下),添加一个c3p0-config.xml文件,配置内容如下:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property
name="jdbcUrl">jdbc:mysql:///jt_db?characterEncoding=utf-8</property>
<property name="user">root</property>
<property name="password">root</property>
</default-config>
</c3p0-config>

这种方式由于是c3p0到指定的位置下寻找指定名称的xml文件,所以文件的位置必须是放在src或其他源码根目录下,文件名必须是c3p0-config.xml。

4、从连接池中获取一个连接对象并进行使用

1
Connection conn = pool.getConnection();

5、用完连接后将连接还回连接池中

1
2
3
4
5
6
JdbcUtil.*close*(conn, ps, rs);
/* 如果是自己创建的连接对象,这个连接对象没有经过任何的改动,调用
* conn.close方法,是将连接对象关闭
* 如果是从连接池中获取的连接对象,该连接对象在返回时就已经被连接池
* 改造了,将连接对象的close方法改为了还连接到连接池中
*/

扩展:切换工作空间,修改默认编码

切换新的工作空间,设置工作空间编码为utf-8

(1)切换到新的工作空间目的:
若旧的工作空间内容过多,可能会导致eclipse不编译,甚至进入休眠状态。

(2)设置工作空间编码为utf-8,此后在当前工作空间下创建的项目编码也默认是utf-8。

eabc894e645afd2837f6fdbcd735eb89

eabc894e645afd2837f6fdbcd735eb89

设置JSP文件的编码为utf-8

8c92de12e98e8b773abbb41133baef4e

8c92de12e98e8b773abbb41133baef4e

扩展:JDBC实现学生信息管理系统

准备数据

建库建表语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 1、创建数据库jt_db数据库(如果不存在才创建)
create database if not exists jt_db charset utf8;
use jt_db; -- 选择jt_db数据库
-- 2、在 jt_db 库中创建 stu 表(学生表)
drop table if exists stu;
create table stu(
id int,
name varchar(50),
gender char(2),
addr varchar(50),
score double
);
-- 3、往 stu 表中, 插入记录
insert into stu values(1001,'张三','男', '北京', 86);

功能实现

运行程序控制台提示如下:

4b9ad722dca6065b01a5202f85e2d89c

4b9ad722dca6065b01a5202f85e2d89c

输入a:查询所有学生信息

输入b:添加学生信息

输入c:根据id修改学生信息

输入d:根据id删除学生信息

查询所有学生信息

在控制台中输入操作代码”a”,效果如下:

55a1ab054846de0d92525b6292f7b930

55a1ab054846de0d92525b6292f7b930

添加学生信息

在控制台中输入操作代码”b”,效果如下:

c9f2e18f2a41668cd39ddaccd712290c

c9f2e18f2a41668cd39ddaccd712290c

根据id修改学生信息

在控制台中输入操作代码”c”,效果如下:

40de75c8a4f08fc4a4b581b358be6a87

40de75c8a4f08fc4a4b581b358be6a87

根据id删除学生信息

在控制台中输入操作代码”d”,效果如下:

9eda8bae1797eced35a1e4907009db83

9eda8bae1797eced35a1e4907009db83

javaweb jdbc