一、JDBC入门
1、什么是JDBC?
JDBC(Java DataBase Connectivity)就是Java数据库连接,说白了就是用Java语言操作数据库。原来我们操作数据库是在控制台使用SQL语句来操作数据库,JDBC是使用Java语言向数据库发送SQL语句。
2、JDBC的原理
早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们开始时发现这是不可能完成的任务,因为各个厂商的数据库差异太大了。后来SUN公司开始于数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后个数据库厂商会遵循SUN的规范提供一套访问自己公司数据库服务的API。SUN公司提供的规范命名为JDBC,而各个厂商提供的遵循了JDBC规范的,可以访问自己数据库的API称之为驱动。
JDBC是接口,而JDBC驱动才是接口的实现,没有驱动无法完成数据库连接,每个数据库厂商都有自己的驱动,用来连接自己公司的数据库。
当然还有第三方公司专门为某一数据库提供驱动,这样的驱动往往不是开元免费的。
3、JDBC核心类(接口)介绍
JDBC的核心类有DriverManager、Connection、Statement和ResultSet。
DriverManager(驱动管理器)的作用有两个:
- 注册驱动:这可以让JDBC知道要使用的是哪个驱动。
- 获取Connection:如果可以获取到Connection,那么说明已经与数据库连接上了。
Connection对象表示链接,与数据库的通讯都通过这个对象展开:
- Connection对象最重要的一个方法就是获取Statement对象。
Statement是用来向数据库发送SQL语句的,这样数据库就会执行发送过来的SQL语句:
- void executeUpdate(String sql):执行更新操作(insert,update,delete等)。
- ResultSet executeQuery(String sql):执行查询操作,数据库在执行查询后会返回查询结果,查询结果就是ResultSet。
ResultSet对象表示查询结果集,只有在执行查询操作后才会有结果集产生,结果集是一个二维表格。操作结果集要学习移动ResultSet内部的"行光标",以及获取当前行上每一列的数据:
- boolean next():使"行光标"移动到下一行,并返回移动后的行是否存在。
- xxx getXxx(int col):获取当前行指定列上的值,参数就是列数,列数从1开始,而不是0。
4、编写一个JDBC程序
下面开始编写第一个JDBC程序:
【1】、mysql数据库驱动jar包:mysql-connector-java-5.1.13-bin.jar
【2】、获取连接
获取连接需要两步,一是使用DriverManager注册驱动,二是使用DriverManager获取connection对象。
- 注册驱动
看清楚了!注册驱动只有一句话:Class.forName(" com.mysql.jdbc.Driver "),下面的内容都是对这句代码的解释。
DriverManager类的registerDriver()方法的参数是java.sql.Driver,但是java.sql.Driver是一个接口,实现类由mysql来提供,MySQL中java.sql.Driver接口的实现类为com.mysql.jdbc.Driver。
注册驱动的代码如下:DriverManager.registerDriver(new com.mysql.jdbc.Driver());
上面的编码虽然可以注册驱动,是出现硬编码(代码以来MySQL驱动jar包),如果将来想要链接Oracle 数据库,那么必须要修改代码。并且其实这种注册驱动的方式是注册了两次驱动。
JDBC中规定,驱动类被加载时,需要手动把自己注册到DriverManager中,下面我们来看看com.mysql.jdbc.Driver类的源码:
<span style="font-family:Arial;font-size:12px;"> public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
……
}</span>
com.mysql.jdbc.driver类中的static块会创建本类对象,并注册到DriverManager中。这说明主要去加载com.mysql.jdbc.Driver类,那么就会执行static块,从而也就把com.mysql.jdbc.driver注册到DriverManager中,所以把注册驱动类的代码改为加载驱动类。
Class.forName("com.mysql.jdbc.Driver");
【3】、获取链接的也只有一段代码:DriverManager.getConnection(url,username,password);其中username和password是登陆数据库的用户名和密码。
url相对复杂一点,它是用来找到要连接的数据库地址,下面是mysql的url:jdbc:mysql://localhost:3306/mydb.
JDBC规定url的格式有三部分组成,每个组成部分使用冒号分隔。
- 第一部分是jdbc,这是固定的;
- 第二部分是数据库名称,连接mysql数据库,第二部分就是mysql;
- 第三部分是由数据库厂商规定的,我们需要了解每个数据库厂商的要求,mysql的第三部分分别由服务器IP(localhost)、端口号(3306)以及DATABASE名称组成。
下面是获取连接的语句:
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb","root","123");
还可以在URL中提供参数:
jdbc:mysql://localhost:3306/mydb?useUnicode=true & characterEncoding=utf8
- useUnicode参数,指定这个数据库链接过程中,使用的是Unicode字节集;
- characterEncoding参数,指定连接数据库的过程中,使用的是UTF8字节集编码。
【4】、获取Statement
在得到Connection之后,说明已经与数据库连接上了,下面是通过Connection获取Statement对象的代码:Statement stmt = con.createStatement();
Statement是用来项数据库发送SQL语句的。
【5】、发送SQL增、删、改语句
String sql = " INSERT INTO stu VALUES(' zhangsan ',' 123 ') ";
int m = stmt.executeUpdate(sql);
其中int类型的返回值,表示执行这条SQL语句所影响的行数,如果SQL语句执行失败,那么executeUpdate方法会抛出一个SQLException异常。
【6】、发送SQL查询语句
String sql = " SELECT * FROM stu ";
ResultSet rs = stmt.executeQuery(sql);
请注意,执行查询使用的不是executeUpdate方法,而是executeQuery方法。executeQuery方法返回的是ResultSet,ResultSet封装了查询结果,称之为结果集。
【7】、读取结果集中的数据
ResultSet就是一张二维的表格,它内部有一个行光标,光标默认的位置在第一行的上方,我们可以调用rs对象的next()方法把" 行光标 "向下移动一行,当第一次调用next方法时," 行光标 "就到了第一行记录的位置,这样可以使用ResultSet提供的getXxx(int col)方法获取指定类的数据了。
rs.next();
rs.getInt(1);
如果不能确定数据的类型,可以使用getObject(int col),或getString(int col)等方法。
【6】、关闭
与IO流一样,使用后的东西都需要关闭,关闭的顺序是先得到的先关闭,后得到的后关闭。
rs.close();
stmt.close();
con.close();
5、代码的规范化
所谓规范化代码,就是无论是否出现异常,都要关闭ResultSet、Statement、以及Connection。
<span style="font-family:Arial;font-size:12px;">public void query{
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try{
con = getConnection();
stmt = con.createStatement();
String sql = "SELECT * FROM user ";
rs = stmt.executeQuery(sql);
while(rs.next()){
String username = rs.getString(1);
String password = rs.getString(2);
}catch(exception e){
throw new RuntimeException(e);
}finally{
try{
if(rs!=null) rs.close();
if(stmt!=null) rs.close();
if(con!=null) rs.close();
}catch(SQLException e){}
}
}
}</span>
二、JDBC对象介绍
1、JDBC中的主要类(接口)
- DriverManager
- Connection
- Statement
- ResultSet
2、DriverManager
其实我们今后只需要会用DriverManager的getConnection()方法即可。
Class.forName(" com.mysql.jdbc.Driver ");
String url = " jdbc:mysql://localhost:3306/mydb ";
String username = " root ";
String password = " 123 ";
Connection con = DriverManager.getConnection(url,username,password);
注意,上面代码可能出现的两个异常:
ClassNotFoundException:这个异常是在第一句出现的,出现这个异常有两个可能,【1】没有给mysql的jar包,【2】类名写错了。
SQLException:这个异常出现在第5句,查看三个参数是否有问题。
对于DriverManager.registerDriver()方法了解即可,因为我们今后注册只会用到Class.forName(),而不会使用这个方法。
3、Connection
Connection最为重要的方法就是获取Statement对象:
-
Statement stmt = con.createStatement();
后面在学习ResultSet时,还要学习一下下面的方法:
- Statement stmt = con.createStatement(int,int);
4、Statement
Statement最为重要的方法就是:
- int executeUpdate(String sql):执行更新操作,即执行insert、update、delete语句,其实这个方法也可以执行create table、alter table、drop table等语句,但我们很少使用JDBC来执行这些语句。
- ResultSet executeQuery(String sql):执行查询操作,返回ResultSet,即结果集。
Statement还有一个boolean execute()方法,可以用来执行增、删、改、查所有SQL语句。该方法返回布尔型值,表示SQL语句是否有结果集。
如果使用execute()方法执行的是更新语句,那么还要调用int getUpdateCount()来获取insert、update、delete语句所影响的行数。
如果使用execute()方法执行的是查询语句,那么还要调用ResultSet getResultSet()来获取select语句的查询结果。
5、ResultSet之滚动结果集(了解)【1】ResultSet提供了一系列的方法来移动" 行光标 ":
-
void beforeFirst():把光标放到第一行的前面,这也是光标默认的位置;
-
void afterLast():把光标放到最后一行的后面;
-
boolean first():把光标放到第一行的位置上,返回值表示调控光标是否成功;
-
boolean last():把光标放到最后一行的位置上;
-
boolean isBeforeFirst():当前光标位置是否在第一行前面;
-
boolean isAfterLast():当前光标位置是否在最后一行的后面;
-
boolean isFirst():当前光标位置是否在第一行上;
-
boolean isLast():当前光标位置是否在最后一行上;
-
boolean previous():把光标向上挪一行;
-
boolean next():把光标向下挪一行;
-
boolean relative(int row):相对位移,当row为正数时,表示向下移动row行,为负数时表示向上移动row行;
-
boolean absolute(int row):绝对位移,把光标移动到指定的行上;
-
int getRow():返回当前光标所有行。
上面的方法分为两类,一类用来判断游标位置的,另一类用来移动游标位置的。
如果结果集是不可滚动的,那么只能使用next()方法来移动游标,而beforeFirst()、afterLast()、first()、last()、previous()、relative()方法都是不可用的!
结果集是否支持滚动,要从Connection类的CreateStatement()方法说起,也就是说创建Statement决定了ResultSet是否支持滚动。
createStatement(int resultSetType,int resultSetConcurrency):
resultSetType参数的可选值:
- ResultSet.TYPE_FORWARD_ONLY:不可滚动结果集;
- ResultSet.TYPE_SCROLL_INSENSTIVE:滚动结果集,但结果集数据不会跟随数据库变化;
- ResultSet.TYPE_SCROLL_SENSITIVE:滚动结果集,结果集数据跟随数据库变化,但没有数据库驱动会支持它。
resultSetConcurrency参数的可选值:
- CONCUR_READ_ONLY:结果集是只读的,不能通过结果集反向影响数据库;
- CONCUR_UPDATABLE:结果集时可更新的,对结果集的更新可以反向影响数据库。
【2】获取结果集元数据:
- 得到元数据:rs.getMetaData(),返回值为ResultSetMetaData;
- 获取结果集列数:int getColumnCount();
- 获取指定列的名:String getColumnName();
【3】结果集特征:当使用Connection的createStatement时,已经确定了statement生成的结果集时什么特征。
- 是否可滚动;
- 是否敏感;
- 是否可更新;
createStatement()生成的结果集:不滚动、不敏感、不可更新。
6、ResultSet之获取列数据
可以通过next()方法使ResultSet游标向下移动,当游标移动到你需要的行时,就可以获取该行的数据了,ResultSet提供了一系列的获取数据的方法:
- String getString(int columnIndex):获取指定列的String类型数据;
- int getInt(int columnIndex):获取指定列的int类型数据;
- double getDouble(int columnIndex):获取指定列的double类型数据;
- boolean getDouble(int columnIndex):获取指定列的boolean类型数据;
- Object getObject(int columnIndex):获取指定列的SObject类型数据;
上面方法中,参数columnIndex表示列的索引,列索引从1开始,而不是0,这一点与数组不同。如果你清楚当前列的数据类型,那么可以使用getInt()方法来获取,如果你不清楚类的类型,那么你应该使用getObject()方法来获取。
ResultSet还提供了一套通过列名来获取列数据的方法:
- String getString(String columnIndex):获取名称为columnIndex的列的String类型数据;
- int getInt(String columnIndex):获取名称为columnIndex的列的int类型数据;
- double getDouble(String columnIndex):获取名称为columnIndex的列的double类型数据;
- boolean getDouble(String columnIndex):获取名称为columnIndex的列的boolean类型数据;
- Object getObject(String columnIndex):获取名称为columnIndex的列的SObject类型数据;
1、什么是SQL攻击
在需要用户输入的地方,用户输入的是SQL语句的片段,最终用户输入的SQL片段与我们DAO中写的SQL语句合成一个完整的SQL语句,
2、演示SQL攻击
首先我们创建一张表,用来存储用户的信息。
CREATE TABLE user(
uid CHAR(32) PRIMARY KEY,
username VARCHAR(30) UNIQUE KEY NOT NULL,
PASSWORD VARCHAR(30)
);
INSERT INTO user VALUES('U_1001', 'zs', 'zs');
SELECT * FROM user;
下面我们写一个login()方法:
public void login(String username, String password) {
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = JdbcUtils.getConnection();
stmt = con.createStatement();
String sql = "SELECT * FROM user WHERE " +
"username='" + username +
"' and password='" + password + "'";
rs = stmt.executeQuery(sql);
if(rs.next()) {
System.out.println("欢迎" + rs.getString("username"));
} else {
System.out.println("用户名或密码错误!");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
JdbcUtils.close(con, stmt, rs);
}
}
下面我们调用这个方法的代码:
login("a' or 'a'='a", "a' or 'a'='a");
这个行当前回事我们登录成功!因为输入的用户名和密码是SQL片段,最终与我们的login()方法的SQL语句组合在一起。组合后的SQL语句:
SELECT * FROM tab_user WHERE username='a' or 'a'='a' and password='a' or 'a'='a'
3、防止SQL攻击
- 过滤用户输入的数据是否包含非法字符;
- 分步校验,想使用用户名查询用户,然后校验密码;
- 使用PreparedStatement;
4、PreparedStatement是什么?
PreparedStatement叫预编译声明。
PreparedStatement是Statement的子接口,可以使用PreparedStatement来代替Statement。
PreparedStatement的好处:
- 防止SQL攻击;
- 提高代码的可读性,以及可维护性;
- 提高效率;
5、PreparedStatement的使用
- 使用Connection的prepareStatement(String sql):即创建它时就让它与一条SQL模板绑定;
- 调用PreparedStatement的setXxx()系列方法为问号设置;
- 调用executeUpdate()或executeQuery()方法,注意调用无参数的方法;
String sql = "SELECT *FROM tab_stuent WHERE s.name=?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1,"s_1001");
ResultSet rs = ps.executeQuery();
rs.close();
ps.clearParameters();
<pre name="code" class="java">ps.setString(1,"s_1002");
ResultSet rs = ps.executeQuery();
在使用Connection创建PreparedStatement对象时需要给出一个sql模板,所谓SQL模板就是有?的SQL语句,其中?代表参数。
在得到PreparedStatement对象后,调用它的setXxx()方法为?赋值,这样就可以把模板变成一条完整的SQL语句,然后再调用PreparedStatement的executeQuery()方法获取ResultSet对象。
注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement对象的executeQuery()是需要参数的(SQL语句)。因为在创建PreparedStatement对象时已经与一条SQL模板绑定在一起了,所以调用它的executeQuery()和executeUpdate()方法时就不需要参数了。
PreparedStatement对象最大的好处是,重复使用同一模板,给与不同的参数来重复的使用它,这才是真正提高效率的原因。
所以,建议大家在今后的开发中,无论什么情况都去使用PreparedStatement,而不是使用Statement。
四、JdbcUtils工具类
1、JdbcUtils的作用:
你也看到了,连接数据库的四大参数:驱动类、URL、用户名、密码,这些参数斗鱼特定的数据库关联,如果将来想要改变数据库,那么就要去修改四大参数,为了不修改代码,我们写一个jdbcUtils类,让他从配置文件中读取配置参数,然后创建连接对象。
2、JdbcUtils代码:
- JdbcUtils.java
public class JdbcUtils{
//配置文件路径
private static final String dbconfig = "dbconfig.properties";
//对应配置文件路径
private static Properties prop = new Properties();
//把配置文件内容加载到prop对象中。因为是放到static块中,所以加载操作只会在JdbcUtils类被加载时完成对配置文件的加载。
static{
try{
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(dbconfig);
prop.load(in);
Class.forName(prop.getProperty("driverClassName"));
}catch(Exception e){
throw new RuntimeException(e);
}
}
public static Connection(){
try{
return DriverManager.getConnection(prop.getProperty("url"),prop.getProperty("username"),prop.getProperty("password"));
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
- dbconfig.properties
<span style="color:#000000;">driverClassName=</span><span style="color:#2a0ff;">com.mysql.jdbc.Driver</span><p><span style="color:#000000;">url=</span><span style="color:#2a0ff;">jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=UTF8</span></p><p><span style="color:#000000;">username=</span><span style="color:#2a0ff;">root</span></p><p><span style="color:#000000;">password=</span><span style="color:#2a0ff;">123</span></p>
五、时间类型
1、Java中的时间类型:
java.sql包下给出三个与数据库相关的日期时间类型,分别是:
- Date:表示日期,只有年月日,没有时分秒,会丢失日期;
- Time:表示时间,只有时分秒,没有年月日,会丢失日期;
- Timestamp:表示时间戳,有年月日时分秒,以及毫秒;
这三个类都是java.util.Date的子类。
- java.util.Date--->java.sql.Date、Time、Timestamp
把util包中的Date转换成毫秒值,使用毫秒值创建sql包中的Date、Time、Timestamp。
java.util.Date date = new java.util.Date();
long l = date.getTime();
java.sql.Date date = new java.sql.Date( l );
- java.sql.Date、Time、Timestamp--->java.util.Date
因为java.sql.Date是java.util.Date的子类,所以这类转换不用处理。
六、大数据
1、什么是大数据?
所谓的大数据,就是大的字节数据,或大的字符数据。标准SQL中提供了如下类型来保存大数据的类型:
类型 | 长度 |
tinyblob | 28--1B(256B) |
blob | 216-1B(64K) |
mediumblob | 224-1B(16M) |
longblob | 232-1B(4G) |
tinyclob | 28--1B(256B) |
clob | 216-1B(64K) |
mediumclob | 224-1B(16M) |
longclob | 232-1B(4G) |
但是SQL中没有提供tinyclob、clob、mediumclob、longclob四种类型,而是使用如下四种类型来处理文本大数据:
类型 | 长度 |
tinytext | 28--1B(256B) |
text | 216-1B(64K) |
mediumtext | 224-1B(16M) |
longtext | 232-1B(4G) |
2、向数据库写数据
首先我们需要创建一张表,表中要有一个mediumblob类型的字段:
CREATE TABLE stu(id INT PRIMARY KEY AUTO_INCREMENT,
filename VARCHAR(100),
data MEDIUMBLOB
);
向数据库插入二进制数据需要使用PreparedStatement对象的setBinaryStream(int,InputStream)方法来完成:
con = JdbcUtils.getConnection();
String sql = "insert into stu(filename,data) values(?,?)";
pstmt = con.prepareStatement(sql);
pstmt.setString(1,"a.jpg");
//得到一个输入流对象
InputStream in = new FileInputStream("c\\:a.jpg")
//为第二个参数复制为流对象
pstmt.setBianryStream(2,in);
pstmt.executeUpdate();
读取二进制数据,需要在查询后使用ResultSet类的getBinaryStream()方法来获取流对象。也就是说PreparedStatement有setXXX(),那么ResultSet就有getXXX()。
con = JdbcUtils.getConnection();
String sql = "select * filename,data from tab_bin where id=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1,1);
rs = pstmt.executeQuery();
String filename = rs.getString("filename");
OutputStream out = new FileOutputStream("f:\\"+filename);
InputStream in = rs.getBinaryStream("data");
IOUtils.copy(in,out);
out.close);
还有一种方法,就是 要把存储的数据包装成Blob类型,然后调用PreparedStatement的setBlob()方法来设置数据:
con = JdbcUtils.getConnection();
String sql = "insert into tab_bin(filename,data) values(?,?)":
pstmt = con.prepareStatement(sql);
pstmt.setString(1,"a.jpg");
File file = new File("f:\\a.jpg");
byte[] datas = FileUtils.getBytes(file);//获取文件中的数据
Blob blob = new SerialBlob(datas);//创建Blob对象
pstmt.setBlob(2,blob);//设置blob类型的参数
pstmt.executeUpdate();
<p><span style="color:#000000;">con = JdbcUtils.</span><span style="color:#000000;">getConnection</span><span style="color:#000000;">();</span></p><p><span style="color:#000000;">String sql = </span><span style="color:#2a0ff;">"select filename,data from tab_bin where id=?"</span><span style="color:#000000;">;</span></p><p><span style="color:#000000;">pstmt = con.prepareStatement(sql);</span></p><p><span style="color:#000000;">pstmt.setInt(1, 1);</span></p><p><span style="color:#000000;">rs = pstmt.executeQuery();</span></p><p><span style="color:#000000;">rs.next();</span></p><p></p><p><span style="color:#000000;">String filename = rs.getString(</span><span style="color:#2a0ff;">"filename"</span><span style="color:#000000;">);</span></p><p><span style="color:#000000;">File file = </span><span style="color:#7f055;">new</span><span style="color:#000000;"> File(</span><span style="color:#2a0ff;">"F:\\"</span><span style="color:#000000;"> + filename) ;</span></p><p><span style="color:#000000;">Blob blob = rs.getBlob(</span><span style="color:#2a0ff;">"data"</span><span style="color:#000000;">);</span></p><p><span style="color:#7f055;">byte</span><span style="color:#000000;">[] datas = blob.getBytes(0, (</span><span style="color:#7f055;">int</span><span style="color:#000000;">)file.length());</span></p><p><span style="color:#000000;">FileUtils.</span><span style="color:#000000;">writeByteArrayToFile</span><span style="color:#000000;">(file, datas);</span></p>
七、批处理
1、Statement批处理
批处理就是一批一批的处理,而不是一个一个的处理。
当你有10条SQL语句要执行时,一次想服务器发送一条SQL语句,这么做效率很差! 处理的方案就是批处理,即一次向服务器发送多条SQL语句,然后由服务器一次性处理。
批处理只针对增、删、改语句,对查询没什么意义。
可以多次调用Statement的addBatch(String sql)方法,把需要执行的所有SQL语句添加到一个" 批 "中,然后调用Statement类的executeBatch()方法来执行当前"批"中的语句。
for(int x=0;x<10;x++){
String number = "s_10"+x;
String name = "stu"+x;
int age = 20+x;
String gender = x%2?"male":"female";
String sql = "insert into stu values('"+number+"','"+name+"',"+age+",'"+gender+"')";
stmt.addBatch(sql);//stmt内部有一个集合,用来装载sql语句
}
stmt.executeBatch();
当执行了"批"之后,"批"中的SQL语句就会被清空!!也就是说连续两次调用executeBatch()相当于调用一次。
还可以在执行"批"之前,调用Statement 的 clearBatch() 的方法来清空" 批 "。
2、PreparedStatement批处理
PreparedStatement的批处理有所不同,因为每个PreparedStatement对象都绑定一条SQL模板。所以向PreparedStatement中添加的不是SQL语句,而是给"?"赋值。
con = JdbcUtils.getConnection();
String sql = "insert into stu values(?,?,?,?)";
pstmt = con.prepareStatement(sql);
for(int x;x<10;x++){
pstmt.setString(1,"s_10"+x);
pstmt.setString(2,"stu"+x);
pstmt.setInt(3,20+x);
pstmt.setString(4,x%2==0?"male":"female");
pstmt.addBatch();
}
pstmt.executeBatch();
八、事务
(一)、事务概述
1、什么是事务?
银行转账!张三转1000元到李四的账户,这其实需要两条SQL语句:给张三的账户减去1000元,给李四的账户加上1000元。
如果在第一条SQL语句执行成功后,在执行第二条SQL语句之前,程序中断(可能抛出某个异常,或其他原因),那么李四的账户没有加上1000元,但是张三的账户减去了1000元,这样肯定不行。
事物的多个操作,要么是完全成功,要么是完全失败,不可能出现成功一半的情况。
2、事务的四大特性
- 原子性(Atomicity):事务中所有操作是不可分隔的原子单位。事务要么完全成功,要么完全失败。
- 一致性(Consistency):事务执行后,数据库状态与其他业务规则保持一致。无论成功与否,参与转账的两个账户余额之和是不变的。
- 隔离性(Isolation):在并发操作中,不同事务之间应该隔离开来,是每个并发中的事务不会互相干扰。
- 持久性(Durability):一旦事务提交成功,事务中的所有数据操作都必须被持久化到数据库中,即使提交事务之后数据库马上崩溃,在数据库重启时,也必须通过某种机制恢复数据。
3、MySQL中的事务
在默认情况下,MySQL每执行一条SQL语句,都是一个单独的事务。如果需要在一个事务中包含多条SQL语句,那么需要开启事务和结束事务。
- 开启事务:start transction;
- 结束事务:commit 或 rollback;
在执行SQL语句之前,先执行start transction,这就开启了一个事务(事务起点),然后可以去执行多条SQL语句。最后结束事务,commit表示提交,即事务中的多条SQL语句所作出的影响会持久化到数据库中。或者,rollback表示回滚,即回滚到事务起点,之前的所有操作都被撤销。
(二)、JDBC事务
在JDBC中处理事务都是通过Connection完成的。
同一事务中所有的操作,都是使用同一个Connection对象!
1、JDBC中的事务
Connection的三个方法与事务相关:
- setAutoCommit(boolean b):设置为是否为自动提交事务,如果true(默认值为true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务;如果设置为false,那么就相当于开启了事务了。
- commit():提交结束事务。
- rollback():回滚结束事务。
JDBC处理事务的代码格式:
try{
con.setAutoCommit(false);//开启事务
...
...
con.commit();//try的最后提交事务
}catch(){
con.rollback();//抛出异常后回滚事务
}
示例:
public void transfer(boolean b){
Connection con = null;
PreparedStatement pstmt = null;
try{
con = JdbcUtils.getConnection();
//设置为手动提交事务,即开启了事务。
con.setAutoCommit(false);
String sql = "update account set balance=balance+? where id=?";
pstmt = con.prepareStatement(sql);
//操作
pstmt.setDouble(1,-10000);
pstmt.setInt(2,1);
pstmt.executeUpdate();
//如果出现了异常就回滚事务。
if(b){
throw new Exception();
}
<pre name="code" class="java"> pstmt.setDouble(1,-10000);
pstmt.setInt(2,1);
pstmt.executeUpdate();
//提交事务
con.commit();
}catch(Exception e){
//回滚事务
if(con!=null){
try{
con.rollback();
}catch(SQLException ex){}
}
throw new RuntimeException();
}finally{
JdbcUtils.close(con,pstmt);
}
}
2、保存点
保存点是JDBC3.0的东西,当要求数据库服务器支持保存点方式的回滚。
检验数据库服务器是否支持保存点:
- Boolean b = con.getMetaData().supportsSavepoints();
保存点的作用是允许事务回滚到指定的保存点位置。在事务中设置好保存点,然后回滚时可以选择回滚到指定的保存点,而不是回滚整个事务。注意:回滚到指定保存点并没有结束事务,只有回滚了整个事务才算结束事务
Connection类的设置保存点,以及回滚到指定保存点方法:
- 设置保存点:SavePoint setSavePoint();
- 回滚到指定保存点:void rollback(Savepoint);
(三)、事务隔离级别
1、事务的并发读问题
- 脏读:读取到另一个事务未提交数据;
- 不可重复读:两次读取不一致;
- 幻读(虚读):读到另一事务已提交数据;
2、并发事务问题
因并发事务导致的问题大致5类,其中两类是更新问题,三类是读问题。
- 脏读(dirty read):读取到另一个事务的未提交数据,即读取到了脏数据;
- 不可重复读(unrepeatable read):对同一记录的两次读取不一致,因为另一事物对该记录做了修改;
- 幻读(phantom read):对同一张表的两次查询不一致,因为另一事务插入了一条记录;
不可重复读和幻读的区别:
- 不可重复读是读取到了另一事务的更新;
- 幻读是读取到了另一事务的插入(MySQL中无法测试到幻读);
3、四大隔离级别
四个等级的事务隔离级别,在相同数据环境下,使用相同的数据输入执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题能力是不同的。
【1】SERIALIZABLE(串行化)
- 不会出现任何并发问题,因为它对同一数据的访问时串行的,非并发访问;
- 性能最差;
【2】REPEATABLE READ(可重复读)---- MySQL
- 防止脏读和不可重复读,不能处理幻读问题;
- 性能比SERIALIZABLE好;
【3】READ COMMITTED(读一提交数据)---- Oracle
- 防止脏读,没有处理不可重复读,也没有处理幻读;
- 性能比REPEATABLE READ好;
【4】READ UNCOMMITTED(读未提交数据)
- 可能出现任何事物并发问题;
- 性能最好;
4、MySQL的隔离级别
MySQL的隔离级别为Repeatable read,可以通过下面语句查看:
- select @@tx_isolation
也可以通过下面语句来设置当前链接的隔离级别:
- set transction isolationleve[ 4选1 ]
5、JDBC设置隔离级别
con.setTransctionIsolation(int level),参数可选值如下:
- Connection.TRANSCTION_READ_UNCOMMITTED;
- Connection.TRANSCTION_READ_COMMITTED;
- Connection.TRANSCTION_REPEATABLE_READ;
- Connection.TRANSCTION_SERIALIZABLE;
九、数据库连接池
(一)、数据库连接池
1、数据库连接池的概念
用池来管理Connection,这样可以重复使用Connection。有了池,所以我们就不用自己来创建Connection,而是通过池来获取Connection对象。当使用完Connection后,调用Connection的close()方法,把Connection归还给池。池就可以再利用这个Connection对象了。
2、JDBC数据库连接池接口(DataSource)
Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商可以让自己的连接池实现这个接口,这样应用程序可以方便的切换不同厂商的连接池。
3、自定义连接池(ItcastPool)
分析:ItcastPool需要有一个List,用来保存连接对象。在ItcastPool的构造器中创建5个连接对象放到List中!当有人调用了ItcastPool的getConnection()方法时,那么就从List拿出一个返回。当List中没有连接可用时,抛出异常。
我们需要对Connection的close()方法进行增强,所以我们需要自定义ItcastConnection类,对Connection进行装饰,即对close()方法进行增强。因为需要在调用close()方法把连接归还给池,所以ItcastConnection类需要拥有对池对象的引用,并且池还要提供归还的方法。
(二)、DBCP
1、什么是DBCP?
DBCP是Apache提供的一款开源免费的数据库连接池。
Hibernate3.0 之后不再对DBCP提供支持,因为Hibernate声明DBCP有致命的缺陷。
2、DBCP的使用
public void fun() throws SQLException{
BasicDataSource ds = new BasicDataSource();
ds.setUsername(" root ");
ds.setPassword(" 123 ");
ds.setUrl(" jdbc:mysql://localhost:3306/mydb ");
ds.setDriverClassName(" com.mysql.jdbc.Driver ");
ds.setMaxActive(20); //最大连接数
ds.setMaxIdle(10); //最大空闲连接数
ds.setInitialSize(10); //初始化连接数
ds.setMinIdle(2); //最小空闲连接数
ds.setMaxWaite(1000); //最大等待毫秒数
Connection con = ds.getConnection();
con.close();
}
3、DBCP的配置信息
- 基本配置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb1
username=root
password=123
- 初始化池大小,即一开始池中就有10个连接对象(默认值为0)
initialSize=0
- 最大连接数,如果设置maxActive = 50时,池中最多可以有50个连接,当然这50个连接中包含被使用的和没被使用的(空闲)
maxActive=8
- 最大空闲连接数,默认值为8,如果设置负数,表示没有吸纳之
maxIdle=8
- 最小空闲连接数,默认值为0
minIdle=0
- 最大等待时间,默认值为-1,表示无限等待,不会抛出异常
maxWait=-1
- 连接属性,就是原来放在URL后面的参数,可以使用connectionProperties来指定,如果已经在URL后面指定了那么就不用再这里指定了。
useServerPrepstmts=true ---- MySQL开启预编译功能
cachePrepstmts=true ---- MySQL开启缓存PreparedStatement功能
prepStmtCacheSize=50 ---- 缓存preparedStatement的上限
preStmtCacheSqlLimit=300 ---- 当SQL模板大于300时,就不用缓存它
- 连接的默认提交方式,默认值为true
defaultAutoCommit=true
- 连接是否为只读连接。Connection有一对方法:setReadOnly(boolean b)和isReadOnly(),如果是只读连接,那么你只能用这个链接来查询,指定连接为只读是为了优化,与并发事务有关。
defaultReadOnly=false
- 指定事务的隔离级别
defaultTransctionIsolation=REPEATABLE_READ
(三)、C3P0
1、C3P0简介
C3P0也是开源免费的连接池!C3P0倍很多人看好!
2、C3P0使用
C3P0中的池类是ComboPooledDataSource
public void function()throws PropertyVetoException,SQLException{
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUser("root");
ds.setPassword("123");
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setAcquireIncrement(5);//每次的增量为5
ds.setInitialPoolSize(20);//初始化连接数
ds.setMinPoolSize(2);//最小连接数
ds.setMaxPoolSize(50);//最大连接数
Connection con = ds.getConnection();
con.close();
}
配置文件要求:
- 文件名称:必须叫c3p0-config.xml
- 文件位置:必须在src下
C3P0也可以指定配置文件,而且配置文件可以是properties,也可以是xml的。但是C3P0的配置文件名必须叫c3p0-config.xml,并在必须放在类路径下。
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<default-config>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb</property>
<pre name="code" class="html">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="acquireIncrement">3</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">2</property>
<property name="maxPoolSize">10</property>
</difault-config>
<named-config name="oracle-donfig">
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb</property>
<pre name="code" class="html"> <property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="acquireIncrement">3</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">2</property>
<property name="maxPoolSize">10</property>
</named-config> </c3p0-config>
C3P0的配置文件中可以配置多个连接信息,可以给每个配置起个名字,这样可以方便通过配置名称来切换配置信息。上面文件中默认配置为mysql配置,名为oracle-config的配置也是mysql的配置。
<p><span style="color:#7f055;">//default默认配置
</span></p><p><span style="color:#7f055;">public</span><span style="color:#000000;"> </span><span style="color:#7f055;">void</span><span style="color:#000000;"> fun2() </span><span style="color:#7f055;">throws</span><span style="color:#000000;"> PropertyVetoException, SQLException {</span></p><p><span style="color:#000000;"> ComboPooledDataSource ds = </span><span style="color:#7f055;">new</span><span style="color:#000000;"> ComboPooledDataSource();</span></p><p><span style="color:#000000;"> Connection con = ds.getConnection();</span></p><p><span style="color:#000000;"> System.</span><span style="color:#00c0;">out</span><span style="color:#000000;">.println(con);</span></p><p><span style="color:#000000;"> con.close();</span></p><p><span style="color:#000000;">}</span></p>
<p><span style="color:#7f055;"><span style="color:#7f055;">//切换为oracle-config配置</span>
</span></p><p><span style="color:#7f055;">public</span><span style="color:#000000;"> </span><span style="color:#7f055;">void</span><span style="color:#000000;"> fun2() </span><span style="color:#7f055;">throws</span><span style="color:#000000;"> PropertyVetoException, SQLException {</span></p><p><span style="color:#000000;"> ComboPooledDataSource ds = </span><span style="color:#7f055;">new</span><span style="color:#000000;"> ComboPooledDataSource(</span><span style="color:#2a0ff;">"orcale-config"</span><span style="color:#000000;">)</span><span style="color:#000000;">;</span></p><p><span style="color:#000000;"> Connection con = ds.getConnection();</span></p><p><span style="color:#000000;"> System.</span><span style="color:#00c0;">out</span><span style="color:#000000;">.println(con);</span></p><p><span style="color:#000000;"> con.close();</span></p><p><span style="color:#000000;">}</span></p>
(四)Tomcat配置连接池
1、Tomcat配置JNDI资源
JNDI(Java Naming and Directory Interface),Java命名和目录接口。
JNDI的作用就是:在服务器上配置资源,然后通过统一的放置来获取配置资源。
下图是Tomcat文档提供的:
配置JNDI资源需要到<Context>元素中配置<Resource>子元素:
- name:指定资源的名称,这个名称可以随便给,在获取资源时需要这个名称;
- factory:用来创建资源的工厂,这个值基本是固定的。不用修改;
- type:资源的类型,我们要给出的类型当然是连接池的类型;
- bar:表示资源的属性,如果资源存在名为bar的属性,那么就配置bar的值。对于DBCP连接池而言,你需要配置的不是bar,因为没有bar属性,而应该去配置url、username等属性。
<p><Context> </p><p> <Resource name="mydbcp" </p><p> type="org.apache.tomcat.dbcp.dbcp.BasicDataSource"</p><p> factory="org.apache.naming.factory.BeanFactory"</p><p> username="root" </p><p> password="123" </p><p> driverClassName="com.mysql.jdbc.Driver" </p><p> url="jdbc:mysql://127.0.0.1/mydb1"</p><p> maxIdle="3"</p><p> maxWait="5000"</p><p> maxActive="5"</p><p> initialSize="3"/></p><p></Context>
</p>
<p><Context> </p><p> <Resource name="myc3p0" </p><p> type="com.mchange.v2.c3p0.ComboPooledDataSource"</p><p> factory="org.apache.naming.factory.BeanFactory"</p><p> user="root" </p><p> password="123" </p><p> classDriver="com.mysql.jdbc.Driver" </p><p> jdbcUrl="jdbc:mysql://127.0.0.1/mydb1"</p><p> maxPoolSize="20"</p><p> minPoolSize ="5"</p><p> initialPoolSize="10"</p><p> acquireIncrement="2"/></p><p></Context>
</p>
2、获取资源
配置资源的目的当然是为了获取资源,只要你启动了Tomcat,那么就可以在项目任何类中通过JDNI获取资源的方式来获取资源了。
下图是Tomcat文档提供的,与上面Tomcat提供的配置资源时对应的:
获取资源:
- Context:javax.naming.Context;
- InitialContext:javax.naming.InitialContext;
- lookup(String str):获取资源的方法,其中" java:comp/env "是资源的入口(这是固定的名称),获取过来的还是一个Context,这说明需要在获取到的Context上进一步进行获取。" bean/MyBeanFactory "对应<Resource>中配置的name值,这回获取的就是资源对象了。
<p><span style="color:#000000;">Context cxt = </span><span style="color:#7f055;">new</span><span style="color:#000000;"> InitialContext(); </span></p><p><span style="color:#000000;">DataSource ds = (DataSource)cxt.lookup(</span><span style="color:#2a0ff;">"java:/comp/env/</span><span style="color:#2a0ff;">mydbcp</span><span style="color:#2a0ff;">"</span><span style="color:#000000;">);</span></p><p><span style="color:#000000;">Connection con = ds.getConnection();</span></p><p><span style="color:#000000;">System.</span><span style="color:#00c0;">out</span><span style="color:#000000;">.println(con);</span></p><p><span style="color:#000000;">con.close();</span></p>
<p><span style="color:#000000;">Context cxt = </span><span style="color:#7f055;">new</span><span style="color:#000000;"> InitialContext(); </span></p><p><span style="color:#000000;">Context </span><span style="color:#000000;">envC</span><span style="color:#000000;">xt =</span><span style="color:#000000;"> (Context)</span><span style="color:#000000;">cxt.lookup(</span><span style="color:#2a0ff;">"java:/comp/env"</span><span style="color:#000000;">);</span></p><p><span style="color:#000000;">DataSource ds = (DataSource)</span><span style="color:#000000;">env</span><span style="color:#000000;">.lookup(</span><span style="color:#2a0ff;">"</span><span style="color:#2a0ff;">mydbcp</span><span style="color:#2a0ff;">"</span><span style="color:#000000;">);</span></p><p><span style="color:#000000;">Connection con = ds.getConnection();</span></p><p><span style="color:#000000;">System.</span><span style="color:#00c0;">out</span><span style="color:#000000;">.println(con);</span></p><p></p><p><span style="color:#000000;">con.close();</span></p>
上面两种方式是相同的效果。
十、ThreadLocal
1、ThreadLocal API
ThreadLocal类只有三个方法:
- void set(T value):保存值
- T get():获取值
- void remove():移除值
2、ThreadLocal的内部是Map
ThreadLocal内部其实是一个Map来保存数据。虽然在使用ThreadLocal时只给出了值,没有给出键,其实它内部使用了当前线程作为键。
class MyThreadLocal<T> {
private Map<Thread,T> map = new HashMap<Thread,T>();
public void set(T value) {
map.put(Thread.currentThread(), value);
}
public void remove() {
map.remove(Thread.currentThread());
}
public T get() {
return map.get(Thread.currentThread());
}
}
十一、DBUtils
1、DBUtils简介
DBUtils是Apache Commons组件的一员,开源免费。
DBUtils是对JDBC的简单封装,但它还是被很多公司使用。
DBUtils的jar包:dbtutils.jar
2、DBUtils主要类
DBUtils都是静态方法,一系列的close()方法。
QueryRunner:
- update():执行insert、update、delete语句;
- query():执行select语句;
- batch():执行批处理;
3、QueryRunner之更新
QueryRunner的update()方法可以用来执行insert、update、delete语句。
【1】创建QueryRunner
构造器:queryRunner()
【2】update方法
int update(Connection con ,String sql ,Object... params)
还有另一种方式来使用QueryRunner。
【1】创建QueryRunner
构造器:QueryRunner( DataSource )
【2】update方法
int update(String sql ,Object... params)
这种方式在创建QueryRunner时传递了连接池对象,那么在调用update()方法时就不用再传递Connection了。
4、ResultSetHandler
我们知道在执行select语句之后得到的时ResultSet,然后我们还需要对ResultSet进行转换,得到我们最终想要的数据。你可以希望把ResultSet的数据放到一个List中,也可以是一个Map中,或者一个Bean中。
DBUtils 提供了一个接口ResultSetHandler,它就是用来ResultSet转换成目标类型的工具,你可以自己实现这个接口,把ResultSet转换成你想要的类型。
DBUtils提供了很多个ResultSetHandler接口的实现,这些实现基本够用,我们通常不用自己去实现ResultSetHandler。
- MapHandler:单行处理器,把结果转为Map<String,Object>,其中列名为键;
- MapListHandler:多行处理器,把结果转换为List<Map<String,Object>>;
- BeanHandler:单行处理器,把结果集转换为Bean,该处理器需要Class参数,即Bean的类型;
- BeanListHandler:多行处理器,把结果集转换为List<Bean>;
- ColumnListHandler:多行单列处理器,把结果集转换为List<Object>,使用ColumnListHandler时需要指定某一列的名称或编号,例如new ColumnListHandler(" name ")表示把name列的数据放到List中;
- ScalarHandler:单行单列处理器,把结果集转换为Object,一般用于聚集查询。
5、QueryRunner之查询
QueryRunner的查询方法是:
- public <T> T query(String sql , ResultSetHandler<T> rh , Object... params);
-
public <T> T query(Connection con, String sql, ResultSetHandler<T> rh, Object… params)
query()方法会通过sql语句和params查询出ResultSet,然后通过rh把ResultSet转换成对应的类型再返回。
6、QueryRunner之批处理
QueryRunner还提供了批处理方法:batch()。
我们更新一行记录时需要指定一个Object[]为参数,如果是批处理,那么就要指定Object[][]为参数了。即多个Object[]就是Object[][]了,其中每个Object[]对应一行记录:
public void fun10() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "insert into tab_student values(?,?,?,?)";
Object[][] params = new Object[10][];[注意,这里是二维数组,这个二维数组有10个一维数组。]//表示 要插入10行记录
for(int i = 0; i < params.length; i++) {
params[i] = new Object[]{"S_300" + i, "name" + i, 30 + i, i%2==0?"男":"女"};
}
qr.batch[执行批处理](sql, params);
}