0%

JavaWeb学习(九)——JDBC

JDBC

——执行SQL语句的java api

操作数据库都是在JDBC API【接口】上,使用不同的数据库,只要用数据库厂商提供的数据库驱动程序即可。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;

try {

/*
* 加载驱动有两种方式
*
* 1:会导致驱动会注册两次,过度依赖于mysql的api,脱离的mysql的开发包,程序则无法编译
* 2:驱动只会加载一次,不需要依赖具体的驱动,灵活性高
*
* 我们一般都是使用第二种方式
* */

//1.
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());

//2.
Class.forName("com.mysql.jdbc.Driver");

//获取与数据库连接的对象-Connetcion
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/zhongfucheng", "root", "root");

//获取执行sql语句的statement对象
statement = connection.createStatement();

//执行sql语句,拿到结果集
resultSet = statement.executeQuery("SELECT * FROM users");

//遍历结果集,得到数据
while (resultSet.next()) {

System.out.println(resultSet.getString(1));

System.out.println(resultSet.getString(2));
}

} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {

/*
* 关闭资源,后调用的先关闭
*
* 关闭之前,要判断对象是否存在
* */

if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}

}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}

}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}

}

}

DriverManager类

Jdbc程序中的DriverManager用于加载驱动,并创建与数据库的链接,这个API的常用方法:

  1. DriverManager.registerDriver(new Driver())
  2. DriverManager.getConnection(url, user, password),

  注意:在实际开发中并不推荐采用registerDriver方法注册驱动。原因有二:
    1、查看Driver的源代码可以看到,如果采用此种方式,会导致驱动程序注册两次,也就是在内存中会有两个Driver对象。
    2、程序依赖mysql的api,脱离mysql的jar包,程序将无法编译,将来程序切换底层数据库将会非常麻烦。

  推荐方式:Class.forName(“com.mysql.jdbc.Driver”);
  采用此种方式不会导致驱动对象在内存中重复出现,并且采用此种方式,程序仅仅只需要一个字符串,不需要依赖具体的驱动,使程序的灵活性更高。

connection类

客户端与数据库所有的交互都是通过Connection来完成的。

常用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建向数据库发送sql的statement对象。

createcreateStatement()

//创建向数据库发送预编译sql的PrepareSatement对象。

prepareStatement(sql)

//创建执行存储过程的callableStatement对象

prepareCall(sql)

//设置事务自动提交

setAutoCommit(boolean autoCommit)

//提交事务

commit()

//回滚事务

rollback()

Statement类

Statement对象用于向数据库发送Sql语句,对数据库的增删改查都可以通过此对象发送sql语句完成。

Statement对象的常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//查询

executeQuery(String sql)

//增删改

executeUpdate(String sql)

//任意sql语句都可以,但是目标不明确,很少用

execute(String sql)

//把多条的sql语句放进同一个批处理中

addBatch(String sql)

//向数据库发送一批sql语句执行

executeBatch()

ResultSet类

ResultSet对象代表Sql语句的执行结果,当Statement对象执行executeQuery()时,会返回一个ResultSet对象

ResultSet对象维护了一个数据行的游标【简单理解成指针】,调用ResultSet.next()方法,可以让游标指向具体的数据行,进行获取该行的数据

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//获取任意类型的数据

getObject(String columnName)

//获取指定类型的数据【各种类型,查看API】

getString(String columnName)

//对结果集进行滚动查看的方法

next()

Previous()

absolute(int row)

beforeFirst()

afterLast()

PreparedStatement类

PreparedStatement对象继承Statement对象,它比Statement对象更强大,使用起来更简单

  1. Statement对象编译SQL语句时,如果SQL语句有变量,就需要使用分隔符来隔开,如果变量非常多,就会使SQL变得非常复杂。PreparedStatement可以使用占位符,简化sql的编写
  2. Statement会频繁编译SQL。PreparedStatement可对SQL进行预编译,提高效率,预编译的SQL存储在PreparedStatement对象中
  3. PreparedStatement防止SQL注入。【Statement通过分隔符’++’,编写永等式,可以不需要密码就进入数据库】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//模拟查询id为2的信息
String id = "2";

Connection connection = UtilsDemo.getConnection();

String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement preparedStatement = connection.preparedStatement(sql);

//第一个参数表示第几个占位符【也就是?号】,第二个参数表示值是多少
preparedStatement.setString(1,id);

ResultSet resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}

//释放资源
UtilsDemo.release(connection, preparedStatement, resultSet);

批处理

当需要向数据库发送一批SQL语句执行时,应避免向数据库一条条发送执行,采用批处理以提升执行效率

批处理有两种方式:

  1. Statement
  2. PreparedStatement

通过executeBath()方法批量处理执行SQL语句,返回一个int[]数组,该数组代表各句SQL的返回值

以下代码是以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
/*
* Statement执行批处理
*
* 优点:
* 可以向数据库发送不同的SQL语句
* 缺点:
* SQL没有预编译
* 仅参数不同的SQL,需要重复写多条SQL
* */
Connection connection = UtilsDemo.getConnection();

Statement statement = connection.createStatement();
String sql1 = "UPDATE users SET name='zhongfucheng' WHERE id='3'";
String sql2 = "INSERT INTO users (id, name, password, email, birthday)" +
" VALUES('5','nihao','123','ss@qq.com','1995-12-1')";

//将sql添加到批处理
statement.addBatch(sql1);
statement.addBatch(sql2);

//执行批处理
statement.executeBatch();

//清空批处理的sql
statement.clearBatch();

UtilsDemo.release(connection, statement, null);

以下方式以PreparedStatement方式实现批处理

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
/*
* PreparedStatement批处理
* 优点:
* SQL语句预编译了
* 对于同一种类型的SQL语句,不用编写很多条
* 缺点:
* 不能发送不同类型的SQL语句
*
* */
Connection connection = UtilsDemo.getConnection();

String sql = "INSERT INTO test(id,name) VALUES (?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);

for (int i = 1; i <= 205; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setString(2, (i + "zhongfucheng"));

//添加到批处理中
preparedStatement.addBatch();

if (i %2 ==100) {

//执行批处理
preparedStatement.executeBatch();

//清空批处理【如果数据量太大,所有数据存入批处理,内存肯定溢出】
preparedStatement.clearBatch();
}

}
//不是所有的%2==100,剩下的再执行一次批处理
preparedStatement.executeBatch();

//再清空
preparedStatement.clearBatch();

UtilsDemo.release(connection, preparedStatement, null);

Oracle

下面用JDBC连接Oracle数据库去操作大文本数据和二进制数据

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//使用JDBC连接Oracle数据库操作二进制数据

/*
* 对于Oracle数据库和Mysql数据库是有所不同的。
* 1.Oracle定义了BLOB字段,但是这个字段不是真正地存储二进制数据
* 2.向这个字段存一个BLOB指针,获取到Oracle的BLOB对象,把二进制数据放到这个指针里面,指针指向BLOB字段
* 3.需要事务支持
*
* */
public class Demo7 {
@Test
public void add() {


Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = UtilsDemo.getConnection();

//开启事务
connection.setAutoCommit(false);

//插入一个BLOB指针
String sql = "insert into test4(id,image) values(?,empty_blob())";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);
preparedStatement.executeUpdate();

//把BLOB指针查询出来,得到BLOB对象
String sql2 = "select image from test4 where id= ? for update";
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.setInt(1, 1);
resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {
//得到Blob对象--当成是Oracle的Blob,不是JDBC的,所以要强转[导的是oracle.sql.BLOB包]
BLOB blob = (BLOB) resultSet.getBlob("image");

//写入二进制数据
OutputStream outputStream = blob.getBinaryOutputStream();

//获取到读取文件读入流
InputStream inputStream = Demo7.class.getClassLoader().getResourceAsStream("01.jpg");

int len=0;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) > 0) {

outputStream.write(bytes, 0, len);
}
outputStream.close();
inputStream.close();
connection.setAutoCommit(true);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
UtilsDemo.release(connection, preparedStatement, null);
}

}

@Test
public void find() {

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = UtilsDemo.getConnection();
String sql = "SELECT * FROM test4 WHERE id=1";

preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {

//获取到BLOB对象
BLOB blob = (BLOB) resultSet.getBlob("image");

//将数据读取到磁盘上
InputStream inputStream = blob.getBinaryStream();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\zhongfucheng.jpg");
int len=0;
byte[] bytes = new byte[1024];

while ((len = inputStream.read(bytes)) > 0) {

fileOutputStream.write(bytes, 0, len);
}

inputStream.close();
fileOutputStream.close();

}

} catch (SQLException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
UtilsDemo.release(connection, preparedStatement, null);
}
}
}

对于JDBC连接Oracle数据库操作CLOB数据,我就不再重复了,操作跟BLOB几乎相同

获取数据库的自动主键

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
@Test
public void test() {

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = JdbcUtils.getConnection();

String sql = "INSERT INTO test(name) VALUES(?)";
preparedStatement = connection.prepareStatement(sql);

preparedStatement.setString(1, "ouzicheng");

if (preparedStatement.executeUpdate() > 0) {

//获取到自动主键列的值
resultSet = preparedStatement.getGeneratedKeys();

if (resultSet.next()) {
int id = resultSet.getInt(1);
System.out.println(id);
}
}

} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(connection, preparedStatement, null);
}

调用数据库的存储过程

调用存储过程的语法:

1
{call <procedure-name>[(<arg1>,<arg2>, ...)]}

调用函数的语法:

1
{?= call <procedure-name>[(<arg1>,<arg2>, ...)]}

如果是Output类型的,那么在JDBC调用的时候是要注册的。如下代码所示:

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
/*
jdbc调用存储过程

delimiter $$
CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam varchar(255))
BEGIN
SELECT CONCAT('zyxw---', inputParam) into inOutParam;
END $$
delimiter ;
*/
//我们在JDBC调用存储过程,就像在调用方法一样
public class Demo9 {

public static void main(String[] args) {
Connection connection = null;
CallableStatement callableStatement = null;

try {
connection = JdbcUtils.getConnection();

callableStatement = connection.prepareCall("{call demoSp(?,?)}");

callableStatement.setString(1, "nihaoa");

//注册第2个参数,类型是VARCHAR
callableStatement.registerOutParameter(2, Types.VARCHAR);
callableStatement.execute();

//获取传出参数[获取存储过程里的值]
String result = callableStatement.getString(2);
System.out.println(result);

} catch (Exception e) {
e.printStackTrace();
}finally {
try {
connection.close();
callableStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

}

参考资料:

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
49
50
51
52
53
54
55
56
57
----------------------------------------------------------------------------------过程

#修改mysql语句的结果符为//
mysql > delimiter //

#定义一个过程,获取users表总记录数,将10设置到变量count中
create procedure simpleproc(out count int)
begin
select count(id) into count from users;
end
//

#修改mysql语句的结果符为;
mysql > delimiter ;

#调用过程,将结果覆给变量a,@是定义变量的符号
call simpleproc(@a);

#显示变量a的值
select @a;

//以下是Java调用Mysql的过程
String sql = "{call simpleproc(?)}";
Connection conn = JdbcUtil.getConnection();
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1,Types.INTEGER);
cstmt.execute();
Integer count = cstmt.getInt(1);
System.out.println("共有" + count + "人");

----------------------------------------------------------------------------------函数

#修改mysql语句的结果符为//
mysql > delimiter //

#定义一个函数,完成字符串拼接
create function hello( s char(20) ) returns char(50)
return concat('hello,',s,'!');
//

#修改mysql语句的结果符为;
mysql > delimiter ;

#调用函数
select hello('world');

//以下是Java调用Mysql的函数
String sql = "{? = call hello(?)}";
Connection conn = JdbcUtil.getConnection();
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1,Types.VARCHAR);
cstmt.setString(2,"zhaojun");
cstmt.execute();
String value = cstmt.getString(1);
System.out.println(value);
JdbcUtil.close(cstmt);
JdbcUtil.close(conn);

数据库连接池

数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个

编写连接池

  1. 编写连接池需实现java.sql.DataSource接口
  2. 创建批量的Connection用LinkedList保存【既然是个池,当然用集合保存、、LinkedList底层是链表,对增删性能较好】
  3. 实现getConnetion(),让getConnection()每次调用,都是在LinkedList中取一个Connection返回给用户
  4. 调用Connection.close()方法,Connction返回给LinkedList
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
private static LinkedList<Connection> list = new LinkedList<>();

//获取连接只需要一次就够了,所以用static代码块
static {
//读取文件配置
InputStream inputStream = Demo1.class.getClassLoader().getResourceAsStream("db.properties");

Properties properties = new Properties();
try {
properties.load(inputStream);
String url = properties.getProperty("url");
String username = properties.getProperty("username");
String driver = properties.getProperty("driver");
String password = properties.getProperty("password");

//加载驱动
Class.forName(driver);

//获取多个连接,保存在LinkedList集合中
for (int i = 0; i < 10; i++) {
Connection connection = DriverManager.getConnection(url, username, password);
list.add(connection);
}

} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}

}

//重写Connection方法,用户获取连接应该从LinkedList中给他
@Override
public Connection getConnection() throws SQLException {
System.out.println(list.size());
System.out.println(list);

//先判断LinkedList是否存在连接
return list.size() > 0 ? list.removeFirst() : null;
}

我们已经完成前三步了,现在问题来了。我们调用Conncetion.close()方法,是把数据库的物理连接关掉,而不是返回给LinkedList的

解决思路:

  1. 写一个Connection子类,覆盖close()方法
  2. 写一个Connection包装类,增强close()方法
  3. 用动态代理,返回一个代理对象出去,拦截close()方法的调用,对close()增强

分析第一个思路:

  • Connection是通过数据库驱动加载的,保存了数据的信息。写一个子类Connection,new出对象,子类的Connction无法直接继承父类的数据信息,也就是说子类的Connection是无法连接数据库的,更别谈覆盖close()方法了。

分析第二个思路:

  • 写一个Connection包装类。
    1. 写一个类,实现与被增强对象的相同接口【Connection接口】
    2. 定义一个变量,指向被增强的对象
    3. 定义构造方法,接收被增强对象
    4. 覆盖想增强的方法
    5. 对于不想增强的方法,直接调用被增强对象的方法
  • 这个思路本身是没什么毛病的,就是实现接口时,方法太多了!,所以我们也不使用此方法

分析第三个思路代码实现:

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
@Override
public Connection getConnection() throws SQLException {

if (list.size() > 0) {
final Connection connection = list.removeFirst();

//看看池的大小
System.out.println(list.size());

//返回一个动态代理对象
return (Connection) Proxy.newProxyInstance(Demo1.class.getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

//如果不是调用close方法,就按照正常的来调用
if (!method.getName().equals("close")) {
return method.invoke(connection, args);
} else {

//进到这里来,说明调用的是close方法
list.add(connection);

//再看看池的大小
System.out.println(list.size());

}
return null;
}

});
}
return null;
}

代码示例:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package me.gacl.demo;

import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.Properties;
import javax.sql.DataSource;
public class JdbcPool implements DataSource{

/**
* @Field: listConnections
* 使用LinkedList集合来存放数据库链接,
* 由于要频繁读写List集合,所以这里使用LinkedList存储数据库连接比较合适
*/
private static LinkedList<Connection> listConnections = new LinkedList<Connection>();

static{
//在静态代码块中加载db.properties数据库配置文件
InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
Properties prop = new Properties();
try {
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
//数据库连接池的初始化连接数大小
int jdbcPoolInitSize =Integer.parseInt(prop.getProperty("jdbcPoolInitSize"));
//加载数据库驱动
Class.forName(driver);
for (int i = 0; i < jdbcPoolInitSize; i++) {
Connection conn = DriverManager.getConnection(url, username, password);
System.out.println("获取到了链接" + conn);
//将获取到的数据库连接加入到listConnections集合中,listConnections集合此时就是一个存放了数据库连接的连接池
listConnections.add(conn);
}

} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}

/* 获取数据库连接
* @see javax.sql.DataSource#getConnection()
*/
@Override
public Connection getConnection() throws SQLException {
//如果数据库连接池中的连接对象的个数大于0
if (listConnections.size()>0) {
//从listConnections集合中获取一个数据库连接
final Connection conn = listConnections.removeFirst();
System.out.println("listConnections数据库连接池大小是" + listConnections.size());
//返回Connection对象的代理对象
return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(!method.getName().equals("close")){
return method.invoke(conn, args);
}else{
//如果调用的是Connection对象的close方法,就把conn还给数据库连接池
listConnections.add(conn);
System.out.println(conn + "被还给listConnections数据库连接池了!!");
System.out.println("listConnections数据库连接池大小为" + listConnections.size());
return null;
}
}
});
}else {
throw new RuntimeException("对不起,数据库忙");
}
}

@Override
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
}

开源数据库连接池

开源组织提供了数据源的独立实现:

  • DBCP 数据库连接池
  • C3P0 数据库连接池

DBCP

Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。

使用DBCP数据源的步骤:

  1. 导入两个jar包【Commons-dbcp.jar和Commons-pool.jar】

  2. 读取配置文件

    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
    #连接设置
    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/jdbcstudy
    username=root
    password=XDP

    #<!-- 初始化连接 -->
    initialSize=10

    #最大连接数量
    maxActive=50

    #<!-- 最大空闲连接 -->
    maxIdle=20

    #<!-- 最小空闲连接 -->
    minIdle=5

    #<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
    maxWait=60000


    #JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
    #注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
    connectionProperties=useUnicode=true;characterEncoding=UTF8

    #指定由连接池所创建的连接的自动提交(auto-commit)状态。
    defaultAutoCommit=true

    #driver default 指定由连接池所创建的连接的只读(read-only)状态。
    #如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
    defaultReadOnly=

    #driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
    #可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
    defaultTransactionIsolation=READ_UNCOMMITTED
  3. 获取BasicDataSourceFactory对象

  4. 创建DataSource对象

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class JdbcUtils_DBCP {    /**
* 在java中,编写数据库连接池需实现java.sql.DataSource接口,每一种数据库连接池都是DataSource接口的实现
* DBCP连接池就是java.sql.DataSource接口的一个具体实现
*/
private static DataSource dataSource = null;
//在静态代码块中创建数据库连接池
static {
try {
//加载dbcpconfig.properties配置文件
InputStream inputStream = Demo3.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
Properties properties = new Properties();
properties.load(inputStream);

//获取工厂对象
BasicDataSourceFactory basicDataSourceFactory = new BasicDataSourceFactory();
//创建数据源
dataSource = basicDataSourceFactory.createDataSource(properties);

} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException {
//从数据源中获取数据库连接
return dataSource.getConnection();

}

//这里释放资源不是把数据库的物理连接释放了,是把连接归还给连接池【连接池的Connection内部自己做好了】
public static void release(Connection conn, Statement st, ResultSet rs) {

if (rs != null) {
try {
//关闭存储查询结果的ResultSet对象
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if (st != null) {
try {
//关闭负责执行SQL命令的Statement对象
st.close();
} catch (Exception e) {
e.printStackTrace();
}

}
if (conn != null) {
try {
//将Connection连接对象还给数据库连接池
conn.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

C3P0

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。C3P0数据源在项目开发中使用得比较多。

  c3p0与dbcp区别

  1. dbcp没有自动回收空闲连接的功能
  2. c3p0有自动回收空闲连接功能

步骤:

  1. 导入开发包【c3p0-0.9.2-pre1.jar】和【mchange-commons-0.2.jar】
  2. 导入XML配置文件【可以在程序中自己一个一个配,C3P0的doc中的Configuration有XML文件的事例】
  3. new出ComboPooledDataSource对象

 c3p0-config.xml的配置信息如下:

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
<?xml version="1.0" encoding="UTF-8"?>
<!--
c3p0-config.xml必须位于类路径下面
private static ComboPooledDataSource ds;
static{
try {
ds = new ComboPooledDataSource("MySQL");
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
-->

<c3p0-config>
<!--
C3P0的缺省(默认)配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource();”这样写就表示使用的是C3P0的缺省(默认)配置信息来创建数据源
-->
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>

<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</default-config>

<!--
C3P0的命名配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource("MySQL");”这样写就表示使用的是name是MySQL的配置信息来创建数据源
-->
<named-config name="MySQL">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>

<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</named-config>

</c3p0-config>

在获取数据库连接的工具类(如jdbcUtils)的静态代码块中创建池

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
49
50
51
52
53
54
55
56
57
58
59
60
61
public class JdbcUtils_C3P0 {

private static ComboPooledDataSource ds = null;
//在静态代码块中创建数据库连接池
static{
try{
//通过代码创建C3P0数据库连接池
/*ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
ds.setUser("root");
ds.setPassword("XDP");
ds.setInitialPoolSize(10);
ds.setMinPoolSize(5);
ds.setMaxPoolSize(20);*/

//通过读取C3P0的xml配置文件创建数据源,C3P0的xml配置文件c3p0-config.xml必须放在src目录下
//ds = new ComboPooledDataSource();//使用C3P0的默认配置来创建数据源
ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置来创建数据源

}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static Connection getConnection() throws SQLException{
//从数据源中获取数据库连接
return ds.getConnection();
}

/**
* 释放的资源包括Connection数据库连接对象,负责执行SQL命令
*/
public static void release(Connection conn,Statement st,ResultSet rs){
if(rs!=null){
try{
//关闭存储查询结果的ResultSet对象
rs.close();
}catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if(st!=null){
try{
//关闭负责执行SQL命令的Statement对象
st.close();
}catch (Exception e) {
e.printStackTrace();
}
}

if(conn!=null){
try{
//将Connection连接对象还给数据库连接池
conn.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

配置Tomcat数据源

Tomcat服务器也给我们提供了连接池,内部其实就是DBCP

步骤:

  1. 在META-INF目录下配置context.xml文件【文件内容可以在tomcat默认页面的 JNDI Resources下Configure Tomcat’s Resource Factory找到】
  2. 导入Mysql或oracle开发包导入到tomcat的lib目录下
  3. 初始化JNDI->获取JNDI容器->检索以XXX为名字在JNDI容器存放的连接池

context.xml文件的配置:

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
<Context>

<Resource name="jdbc/EmployeeDB"
auth="Container"
type="javax.sql.DataSource"

username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/zhongfucheng"
maxActive="8"
maxIdle="4"/>
</Context>
try {

//初始化JNDI容器
Context initCtx = new InitialContext();

//获取到JNDI容器
Context envCtx = (Context) initCtx.lookup("java:comp/env");

//扫描以jdbc/EmployeeDB名字绑定在JNDI容器下的连接池
DataSource ds = (DataSource)
envCtx.lookup("jdbc/EmployeeDB");

Connection conn = ds.getConnection();
System.out.println(conn);

}

在获取数据库连接的工具类(如jdbcUtils)的静态代码块中获取JNDI容器中的数据源:

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
49
50
public class JdbcUtils_JNDI {

private static DataSource ds = null;
//在静态代码块中创建数据库连接池
static{
try{
//初始化JNDI
Context initCtx = new InitialContext();
//得到JNDI容器
Context envCtx = (Context) initCtx.lookup("java:comp/env");
//从JNDI容器中检索name为jdbc/datasource的数据源
ds = (DataSource)envCtx.lookup("jdbc/datasource");
}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static Connection getConnection() throws SQLException{
//从数据源中获取数据库连接
return ds.getConnection();
}
public static void release(Connection conn,Statement st,ResultSet rs){
if(rs!=null){
try{
//关闭存储查询结果的ResultSet对象
rs.close();
}catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if(st!=null){
try{
//关闭负责执行SQL命令的Statement对象
st.close();
}catch (Exception e) {
e.printStackTrace();
}
}

if(conn!=null){
try{
//将Connection连接对象还给数据库连接池
conn.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

使用dbutils框架

dbutils它是对JDBC的简单封装,极大简化jdbc编码的工作量

commons-dbutilsAPI介绍:

  • org.apache.commons.dbutils.QueryRunner
  • org.apache.commons.dbutils.ResultSetHandler

工具类:

  • org.apache.commons.dbutils.DbUtils

DbUtils类

提供了关闭连接,装载JDBC驱动,回滚提交事务等方法的工具类【比较少使用,因为我们学了连接池,就应该使用连接池连接数据库】

QueryRunner类

该类简化了SQL查询,配合ResultSetHandler使用,可以完成大部分的数据库操作,重载了许多的查询,更新,批处理方法。大大减少了代码量

ResultSetHandler接口

该接口规范了对ResultSet的操作,要对结果集进行什么操作,传入ResultSetHandler接口的实现类即可。

  • ArrayHandler:把结果集中的第一行数据转成对象数组。
  • ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
  • BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。
  • BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。
  • ColumnListHandler:将结果集中某一列的数据存放到List中。
  • KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里,再把这些map再存到一个map里,其key为指定的key。
  • MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。
  • MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List
  • ScalarHandler 将ResultSet的一个列到一个对象中。

使用DbUtils框架对数据库的CRUD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 使用DbUtils框架对数据库的CRUD
* 批处理
*
* */
public class Test {

@org.junit.Test
public void add() throws SQLException {

//创建出QueryRunner对象
QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource());
String sql = "INSERT INTO student (id,name) VALUES(?,?)";

//我们发现query()方法有的需要传入Connection对象,有的不需要传入
//区别:你传入Connection对象是需要你来销毁该Connection,你不传入,由程序帮你把Connection放回到连接池中
queryRunner.update(sql, new Object[]{"100", "zhongfucheng"});

}

@org.junit.Test
public void query()throws SQLException {

//创建出QueryRunner对象

事务

一个SESSION所进行的所有更新操作要么一起成功,要么一起失败

注意:当Connection遇到一个未处理的SQLException时,系统会非正常退出,事务也会自动回滚,但如果程序捕获到了异常,是需要在catch中显式回滚事务的。

savapoint

可以设置中间点。

注意:savepoint不会结束当前事务,普通提交和回滚都会结束当前事务的

注事务的隔离级别

数据库定义了4个隔离级别:

  1. Serializable【可避免脏读,不可重复读,虚读】
  2. Repeatable read【可避免脏读,不可重复读】
  3. Read committed【可避免脏读】
  4. Read uncommitted【级别最低,什么都避免不了】

分别对应Connection类中的4个常量

  1. TRANSACTION_READ_UNCOMMITTED
  2. TRANSACTION_READ_COMMITTED
  3. TRANSACTION_REPEATABLE_READ
  4. TRANSACTION_SERIALIZABLE

脏读:一个事务读取到另外一个事务未提交的数据

不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改

虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。

注:和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致


简单总结:脏读是不可容忍的,不可重复读和虚读在一定的情况下是可以的【做统计的肯定就不行】

事务处理

  • 在数据访问层(Dao)处理事务

    Dao层只涉及到最基本的CRUD,不涉及具体的业务操作,所以在开发中DAO层出现这样的业务处理方法是一种不好的设计。

  • 在业务层(BusinessService)处理事务

  • 使用ThreadLocal进行事务处理

分页

Oracle实现分页

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
49
50
/*
Oracle分页:
Oracle的分页依赖于ROWNUM这个伪列,ROWNUM主要作用就是产生行号。

分页原理:
1:子查询查出前n行数据,ROWNUM产生前N行的行号
2:使用子查询产生ROWNUM的行号,通过外部的筛选出想要的数据

例子:
我现在规定每页显示5行数据【lineSize=5】,我要查询第2页的数据【currentPage=2】
注:【对照着语法来看】

实现:
1:子查询查出前10条数据【ROWNUM<=10】
2:外部筛选出后面5条数据【ROWNUM>5】
3:这样我们就取到了后面5条的数据
*/
/*
Oracle分页语法:
@lineSize---每页显示数据行数
/*
Oracle分页:
Oracle的分页依赖于ROWNUM这个伪列,ROWNUM主要作用就是产生行号。

分页原理:
1:子查询查出前n行数据,ROWNUM产生前N行的行号
2:使用子查询产生ROWNUM的行号,通过外部的筛选出想要的数据

例子:
我现在规定每页显示5行数据【lineSize=5】,我要查询第2页的数据【currentPage=2】
注:【对照着语法来看】

实现:
1:子查询查出前10条数据【ROWNUM<=10】
2:外部筛选出后面5条数据【ROWNUM>5】
3:这样我们就取到了后面5条的数据
*/

/*
Oracle分页语法:
@lineSize---每页显示数据行数
@currentPage----当前所在页

*/
SELECT *FROM (
SELECT 列名,列名,ROWNUM rn
FROM 表名
WHERE ROWNUM<=(currentPage*lineSize)) temp

WHERE temp.rn>(currentPage-1)*lineSize;

Mysql实现分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /*
Mysql分页语法:
@start---偏移量,不设置就是从0开始【也就是(currentPage-1)*lineSize】
@length---长度,取多少行数据

*/
SELECT *
FROM 表名
LIMIT [START], length;

/*
例子:
我现在规定每页显示5行数据,我要查询第2页的数据

分析:
1:第2页的数据其实就是从第6条数据开始,取5条

实现:
1:start为5【偏移量从0开始】
2:length为5

*/

总结:

  • Mysql从(currentPage-1)*lineSize开始取数据,取lineSize条数据
  • Oracle先获取currentPage*lineSize条数据,从(currentPage-1)*lineSize开始取数据

面试题

参考文章