概述
JDBC(Java DataBase Connectivity)就是Java数据库连接,说白了就是用Java语言来操作数据库。原来我们操作数据库是在控制台使用SQL语句来操作数据库,JDBC是用Java语言向数据库发送SQL语句。
早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。
后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的规范命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!
JDBC主要接口和类
在JDBC中常用的类有:
- DriverManager – 类,用来获取Connection
- Connection – 接口
- Statement – 接口
- ResultSet – 接口
DriverManager(重点)
其实我们今后只需要会用DriverManager的getConnection()方法即可:
Class.forName(“oracle.jdbc.OracleDriver”);//注册驱动
String url = “jdbc:oracle:thin:@127.0.0.1:1521:orcl”;
String username = “scott”;
String password = “tiger”;
Connection con = DriverManager.getConnection(url, username, password);
假设一个类以前从来没有被装进内存过,Class.forName(String className)这个方法会做以下几件事情:
- 装载。将字节码读入内存,并产生一个与之对应的java.lang.Class类对象
- 连接。这一步会验证字节码,为static变量分配内存,并赋默认值(0或null),并可选的解析符号引用(这里不理解没关系)
- 初始化。为类的static变量赋初始值,假如有static int a = 1;这个将a赋值为1的操作就是这个时候做的。除此之外,还要调用类的static块。(这一步是要点)
而这里的Class.forName(“oracle.jdbc.OracleDriver”)
主要就是为了执行下面这个static代码块
static {
Timestamp localTimestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
try {
if (defaultDriver == null) {
defaultDriver = new OracleDriver();
DriverManager.registerDriver(defaultDriver);
}
}
catch (RuntimeException localRuntimeException) {
}
catch (SQLException localSQLException){}
_Copyright_2004_Oracle_All_Rights_Reserved_ = null;
}
可以看到主要是为了执行以下的代码
defaultDriver = new OracleDriver();
DriverManager.registerDriver(defaultDriver);
所以Class.forName(“oracle.jdbc.OracleDriver”)
可以被以下代码替换
DriverManager.registerDriver(new OracleDriver());
注意,上面连接数据库的代码可能出现的两种异常:
1.ClassNotFoundException:这个异常是在第1句上出现的,出现这个异常有两个可能:
- 没有给出oracle的jar包;
- 把类名称打错了,查看类名是不是oracle.jdbc.OracleDriver。
2.SQLException:这个异常出现在第5句,出现这个异常就是三个参数的问题,往往username和password一般不是出错,所以需要认真查看url是否打错。
Connection
Connection最为重要的方法就是获取Statement:
Statement stmt = con.createStatement();
后面在学习ResultSet方法时,还要学习一下下面的方法:
Statement stmt = con.createStatement(int,int);
Statement
Statement最为重要的方法是:
方法 | 描述 |
---|---|
int executeUpdate(String sql) | 执行更新操作,即执行insert、update、delete语句,其实这个方法也可以执行create table、alter table,以及drop table等语句,但我们很少会使用JDBC来执行这些语句 |
ResultSet executeQuery(String sql) | 执行查询操作,执行查询操作会返回ResultSet,即结果集 |
boolean execute() | 这个方法可以用来执行增、删、改、查所有SQL语句。该方法返回的是boolean类型,表示SQL语句是否执行成功 |
对于boolean execute()
,还有一些地方需要注意:
-
如果使用
execute()
方法执行的是更新语句,那么还要调用int getUpdateCount()
来获取insert、update、delete语句所影响的行数。 -
如果使用
execute()
方法执行的是查询语句,那么还要调用ResultSet getResultSet()
来获取select语句的查询结果。
ResultSet
可以通过next()
方法使ResultSet的游标向下移动,当游标移动到你需要的行时,就需要来获取该行的数据了,ResultSet提供了一系列的获取列数据的方法:
- String getString(int columnIndex):获取指定列的String类型数据;
- int getInt(int columnIndex):获取指定列的int类型数据;
- double getDouble(int columnIndex):获取指定列的double类型数据;
- Object getObject(int columnIndex):获取指定列的Object类型的数据。
上面方法中,参数columnIndex表示列的索引,列索引从1开始,而不是0,这第一点与数组不同。如果你清楚当前列的数据类型,那么可以使用getInt()
之类的方法来获取,如果你不清楚列的类型,那么你应该使用getObject()
方法来获取。
ResultSet还提供了一套通过列名称来获取列数据的方法:
- String getString(String columnName):获取名称为columnName的列的String数据;
- int getInt(String columnName):获取名称为columnName的列的int数据;
- double getDouble(String columnName):获取名称为columnName的列的double数据;
- Object getObject(String columnName):获取名称为columnName的列的Object数据;
结果集的列数
ResultSet rs = stm.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();
int count = rsmd.getColumnCount();
获取行数只能通过滚动resultset
ResultSet rs = stm.executeQuery(sql);
rs.last();
int row = rs.getRow();
JDBC应用示例
引入数据库的驱动jar包
Oracle:ojdbc14-10.2.0.2.0.jar
Mysql: mysql-connector-java-5.1.13-bin.jar;
获取连接
获取连接需要两步,一是使用DriverManager来注册驱动,二是使用DriverManager来获取Connection对象。
注册驱动
注册驱动就只有一句话:
Class.forName(“oracle.jdbc.OracleDriver”)
获取连接
获取连接的也只有一句代码:
DriverManager.getConnection(url,username,password)
其中username和password是登录数据库的用户名和密码。
url查对复杂一点,它是用来找到要连接数据库“网址”,就好比你要浏览器中查找百度时,也需要提供一个url。
oracle的url
jdbc:oracle:thin:@[ip]:[port]:orcl
mysql的url
jdbc:mysql://[ip]:[prot]/[datebase]
JDBC规定url的格式由三部分组成,每个部分中间使用冒号分隔
- 第一部分是jdbc,这是固定的;
- 第二部分是数据库名称,那么连接mysql数据库,第二部分当然是mysql了;
- 第三部分是由数据库厂商规定的,我们需要了解每个数据库厂商的要求,mysql的第三部分分别由数据库服务器的IP地址(localhost)、端口号(3306),以及DATABASE名称(mydb1)组成。
下面是获取连接的语句
Connection con = DriverManager.getConnection(“jdbc:oracle:thin:@127.0.0.1:1521:orcl
”,”username”,”password”);
获取Statement
在得到Connectoin之后,说明已经与数据库连接上了,下面是通过Connection获取Statement对象的代码:
Statement stmt = con.createStatement();
Statement是用来向数据库发送要执行的SQL语句的!
发送SQL增、删、改、查语句
发送SQL添加语句
String sql = “insert into user value(’zhangSan’, ’123’)”;
int m = stmt.executeUpdate(sql);
其中int类型的返回值表示执行这条SQL语句所影响的行数,我们知道,对insert来说,最后只能影响一行,而update和delete可能会影响0~n行。
如果SQL语句执行失败,那么executeUpdate()
会抛出一个SQLException。
发送SQL查询语句
String sql = “select * from user”;
ResultSet rs = stmt.executeQuery(sql);
注意,执行查询使用的不是executeUpdate()方法,而是executeQuery()方法。executeQuery()方法返回的是ResultSet,ResultSet封装了查询结果,我们称之为结果集。
ResultSet就是一张二维的表格,它内部有一个“行光标”,光标默认的位置在“第一行上方”,我们可以调用rs对象的next()
方法把“行光标”向下移动一行,当第一次调用next()
方法时,“行光标”就到了第一行记录的位置,这时就可以使用ResultSet提供的getXXX(int col)
方法来获取指定列的数据了:
rs.next();//光标移动到下一行
rs.getInt(1);//获取第一行第一列的数据
当你使用rs.getInt(1)
方法时,你必须可以肯定第1列的数据类型就是int类型,如果你不能肯定,那么最好使用rs.getObject(1)
。在ResultSet类中提供了一系列的getXXX()
方法,比较常用的方法有:
Object getObject(int col)
String getString(int col)
int getInt(int col)
double getDouble(int col)
关闭资源
与IO流一样,使用后的东西都需要关闭!关闭的顺序是先得到的后关闭,后得到的先关闭。
rs.close();
stmt.close();
con.close();
整体流程代码
public void show() {
//4大参数,driverClassName(每个数据库不一样) / url(针对不同的数据库连接格式不同) /
//username(数据库用户名) / password(数据库密码)
String driverClassNameString = "oracle.jdbc.OracleDriver";
String url = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
String username = "scott";
String password = "tiger";
Connection con=null;
ResultSet rs=null;
Statement statement=null;
try{
//注册驱动类
Class.forName(driverClassNameString);
//得到连接器,基础
con = DriverManager.getConnection(url,username,password);
//语句集
statement = con.createStatement();
// 执行内容字符串化
// 插入数据
// String sql = "insert into stu values(null,'john',99,99,99);";
// 改表名
// String sql = "rename table stu to student;";
// 删除
// String sql = "delete from student where id=5;";
// 修改信息 update
// int i = statement.executeUpdate(sql);
// System.out.println("插入到"+i);
// 用到查询就需要返回字符集
String sql = "select * from student;";
rs = statement.executeQuery(sql);
while(rs.next()) {
System.out.println(rs.getString(1) + ", " + rs.getString(2)
+ ", " + rs.getString(3)+","+rs.getString(4)+","+rs.getString(5));
}
}catch (Exception e) {
throw new RuntimeException(e);
}finally {
try{
if(rs != null) rs.close();
if(statement != null) statement.close();
if(con != null) con.close();
}catch(Exception e) {
e.printStackTrace();
}
}
}
JDBC预编译
SQL注入问题
在需要用户输入的地方,用户输入的是SQL语句的片段,最终用户输入的SQL片段与我们DAO中写的SQL语句合成一个完整的SQL语句。
例如用户在登录时输入的用户名和密码都是为SQL语句的片段。
演示SQL注入
首先我们需要创建一张用户表,用来存储用户的信息。
下面我们写一个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'
这样就不能实现密码验证的要求,同时还会出现一些异常情况
防止SQL注入的方法
- 过滤用户输入的数据中是否包含非法字符
- 使用PreparedStatement
PreparedStatement
PreparedStatement叫预编译声明。
PreparedStatement是Statement的子接口,你可以使用PreparedStatement来替换Statement。
PreparedStatement的好处:
- 防止SQL攻击
- 提高代码的可读性,以可维护性
- 提高效率
使用示例
String sql = “select * from tab_student where s_number=?”;
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, “S_1001”);
ResultSet rs = pstmt.executeQuery();
rs.close();
pstmt.clearParameters();
pstmt.setString(1, “S_1002”);
rs = pstmt.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工具类
连接数据库的四大参数是:驱动类、url、用户名,以及密码。
这些参数都与特定数据库关联,如果将来想更改数据库,那么就要去修改这四大参数,那么为了不去修改代码,我们写一个JdbcUtils类,让它从配置文件中读取配置参数,然后创建连接对象。
工具类如下
public class JdbcUtils {
private static final String dbconfig = "dbconfig.properties";
private static Properties prop = new Properties();
static {
try {
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(dbconfig);
prop.load(in);
Class.forName(prop.getProperty("driverClassName"));
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() {
try {
return DriverManager.getConnection(prop.getProperty("url"),
prop.getProperty("username"), prop.getProperty("password"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
配置文件如下
driverClassName=oracle.jdbc.OracleDriver
url= jdbc:oracle:thin:@127.0.0.1:1521:orcl
username=scott
password=tiger
时间类型
Java中的时间类型
java.sql包下给出三个与数据库相关的日期时间类型,分别是:
- Date:表示日期,只有年月日,没有时分秒。会丢失时间;
- Time:表示时间,有年月日时分秒;
- Timestamp:表示时间戳,有年月日时分秒,以及毫秒。
这三个类都是java.util.Date的子类
- java.util.Date – 年月日时分秒
- java.util.Calendar – Date getTime()
需要注意的是传入SQL中的时间类型一定要是java.sql包下的,在平常使用的时间类型则要是java.util包下的。
时间类型相互转换
把数据库的三种时间类型赋给java.util.Date,基本不用转换,因为这是把子类对象给父类的引用,不需要转换。
java.sql.Date date = …
java.util.Date d = date;
java.sql.Time time = …
java.util.Date d = time;
java.sql.Timestamp timestamp = …
java.util.Date d = timestamp;
当需要把java.util.Date转换成数据库的三种时间类型时,这就不能直接赋值了,这需要使用数据库三种时间类型的构造器。
java.sql包下的Date、Time、TimeStamp三个类的构造器都需要一个long类型的参数,表示毫秒值。
创建这三个类型的对象,只需要有毫秒值即可。我们知道java.util.Date有getTime()方法可以获取毫秒值,那么这个转换也就不是什么问题了。
java.utl.Date d = new java.util.Date();
java.sql.Date date = new java.sql.Date(d.getTime());//会丢失时分秒
Time time = new Time(d.getTime());
Timestamp timestamp = new Timestamp(d.getTime());
应用示例
创建一个dt表
CREATE TABLE dt(
d DATE,
t TIME,
ts TIMESTAMP
)
向dt表中插入数据
@Test
public void fun1() throws SQLException {
Connection con = JdbcUtils.getConnection();
String sql = "insert into dt value(?,?,?)";
PreparedStatement pstmt = con.prepareStatement(sql);
java.util.Date d = new java.util.Date();
pstmt.setDate(1, new java.sql.Date(d.getTime()));
pstmt.setTime(2, new Time(d.getTime()));
pstmt.setTimestamp(3, new Timestamp(d.getTime()));
pstmt.executeUpdate();
}
从dt表中查询数据
@Test
public void fun2() throws SQLException {
Connection con = JdbcUtils.getConnection();
String sql = "select * from dt";
PreparedStatement pstmt = con.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
rs.next();
java.util.Date d1 = rs.getDate(1);
java.util.Date d2 = rs.getTime(2);
java.util.Date d3 = rs.getTimestamp(3);
System.out.println(d1);
System.out.println(d2);
System.out.println(d3);
}
大数据类型
概述
所谓大数据,就是大的字节数据,或大的字符数据。标准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) |
在mysql中没有提供tinyclob、clob、mediumclob、longclob四种类型,而是使用如下四种类型来处理文本大数据:
类型 | 长度 |
---|---|
tinytext | 28–1B(256B) |
text | 216-1B(64K) |
mediumtext | 224-1B(16M) |
longtext | 232-1B(4G) |
应用示例
创建一张表,表中要有一个mediumblob(16M)类型的字段。
CREATE TABLE tab_bin(
id INT PRIMARY KEY AUTO_INCREMENT,
filename VARCHAR(100),
data MEDIUMBLOB
);
向数据库插入二进制数据需要使用PreparedStatement为原setBinaryStream(int, InputSteam)
方法来完成。
con = JdbcUtils.getConnection();
String sql = "insert into tab_bin(filename,data) values(?, ?)";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, "a.jpg");
InputStream in = new FileInputStream("f:\\a.jpg");
pstmt.setBinaryStream(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();
rs.next();
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();
接收
con = JdbcUtils.getConnection();
String sql = "select filename,data from tab_bin where id=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, 1);
rs = pstmt.executeQuery();
rs.next();
String filename = rs.getString("filename");
File file = new File("F:\\" + filename) ;
Blob blob = rs.getBlob("data");
byte[] datas = blob.getBytes(0, (int)file.length());
FileUtils.writeByteArrayToFile(file, datas);
批处理
批处理就是一批一批的处理,而不是一个一个的处理!
当你有10条SQL语句要执行时,一次向服务器发送一条SQL语句,这么做效率上很差!处理的方案是使用批处理,即一次向服务器发送多条SQL语句,然后由服务器一次性处理。
PreparedStatement的批处理有所不同,因为每个PreparedStatement对象都绑定一条SQL模板。所以向PreparedStatement中添加的不是SQL语句,而是给“?”赋值。
addBatch();
示例如下
con = JdbcUtils.getConnection();
String sql = "insert into stu values(?,?,?,?)";
pstmt = con.prepareStatement(sql);
for(int i = 0; i < 10; i++) {
pstmt.setString(1, "S_10" + i);
pstmt.setString(2, "stu" + i);
pstmt.setInt(3, 20 + i);
pstmt.setString(4, i % 2 == 0 ? "male" : "female");
pstmt.addBatch();
}
pstmt.executeBatch();
事务
概述
银行转账!张三转1000块到李四的账户,这其实需要两条SQL语句:
- 给张三的账户减去1000元;
- 给李四的账户加上1000元。
如果在第一条SQL语句执行成功后,在执行第二条SQL语句之前,程序被中断了(可能是抛出了某个异常,也可能是其他什么原因),那么李四的账户没有加上10000元,而张三却减去了10000元。这肯定是不行的!
你现在可能已经知道什么是事务了吧!事务中的多个操作,要么完全成功,要么完全失败!不可能存在成功一半的情况!也就是说给张三的账户减去10000元如果成功了,那么给李四的账户加上10000元的操作也必须是成功的;否则给张三减去10000元,以及给李四加上10000元都是失败的!
事务的四大特性(ACID)
- 原子性(Atomicity):事务中所有操作是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。
- 一致性(Consistency):事务执行后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
- 隔离性(Isolation):隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
- 持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。
Oracle中的事务
**在默认情况下,每执行一条增、删、改SQL语句,都是一个单独的事务。**如果需要在一个事务中包含多条SQL语句,那么需要开启事务和结束事务。
结束事务:commit或rollback。
在执行增、删、改一条SQL就开启了一个事务(事务的起点),然后可以去执行多条SQL语句,最后要结束事务,commit表示提交,即事务中的多条SQL语句所做出的影响会持久化到数据库中。或者rollback,表示回滚,即回滚到事务的起点,之前做的所有操作都被撤消了。
回滚
UPDATE account SET balance=balance-10000 WHERE id=1;
UPDATE account SET balance=balance+10000 WHERE id=2;
ROLLBACK;
提交
UPDATE account SET balance=balance-10000 WHERE id=1;
UPDATE account SET balance=balance+10000 WHERE id=2;
COMMIT;
JDBC事务
Connection的三个方法与事务相关:
- setAutoCommit(boolean):设置是否为自动提交事务,如果true(默认值就是true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置false,那么就相当于开启了事务了;
- commit():提交结束事务;
- 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();
}
pstmt.setDouble(1, 10000);
pstmt.setInt(2, 2);
pstmt.executeUpdate();
//提交事务
con.commit();
} catch(Exception e) {
//回滚事务
if(con != null) {
try {
con.rollback();
} catch(SQLException ex) {}
}
throw new RuntimeException(e);
} finally {
//关闭
JdbcUtils.close(con, pstmt);
}
}
事务隔离级别
五大并发事务问题
因为并发事务导致的问题大致有5类,其中两类是更新问题,三类是读问题。
- 脏读(dirty read):读到未提交更新数据
A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操作。但是接着B事务撤销了所做的更新,这会导致A事务操作的是脏数据。(这是绝对不允许出现的事情)
- 虚读(幻读)(phantom read):读到已提交插入数据
A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这导致两次查询结果不同。(在实际开发中,很少会对相同数据进行两次查询,所以可以考虑是否允许虚读)
- 不可重复读(unrepeatable read):读到已提交更新数据
不可重复读与虚读有些相似,都是两次查询的结果不同。后者是查询到了另一个事务已提交的新插入数据,而前者是查询到了另一个事务已提交的更新数据。
四大隔离级别
4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。
不同事务隔离级别能够解决的数据并发问题的能力是不同的。
- SERIALIZABLE(串行化)
当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。
当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。
- REPEATABLE READ(可重复读)
当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。
- READ COMMITTED(读已提交数据)
当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。
- READ UNCOMMITTED(读未提交数据)
当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。
四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!
MySQL的默认隔离级别为REPEATABLE READ。
Oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别。所以Oracle不支持脏读的默认隔离级别是READ COMMITTED。
JDBC设置隔离级别
con. setTransactionIsolation(int level)
参数可选值如下:
- Connection.TRANSACTION_READ_UNCOMMITTED
- Connection.TRANSACTION_READ_COMMITTED
- Connection.TRANSACTION_REPEATABLE_READ
- Connection.TRANSACTION_SERIALIZABLE
JDBC数据库连接池
概述
用池来管理Connection,就可以重复使用Connection。有了池,所以我们就不用自己来创建Connection,而是通过池来获取Connection对象。
当使用完Connection后,调用Connection的close()
方法也不会真的关闭Connection,而是把Connection“归还”给池。池就可以再利用这个Connection对象了。
Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商可以让自己的连接池实现这个接口。
这样应用程序可以方便的切换不同厂商的连接池!
DBCP连接池
DBCP是Apache提供的一款开源免费的数据库连接池!
Hibernate3.0之后不再对DBCP提供支持!因为Hibernate声明DBCP有致命的缺欠!DBCP因为Hibernate的这一毁谤很是生气,并且说自己没有缺欠。
使用示例
通过配置文件创建连接池
public static void main(String[] args) {
Properties prop = new Properties();
InputStream in = JDBCPool.class.getClassLoader().getResourceAsStream("dbcp.properties");
try {
prop.load(in);
DataSource ds = BasicDataSourceFactory.createDataSource(prop);
Connection conn = ds.getConnection();
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
properties文件
#基本配置
driverClassName=oraclel.jdbc.OracleDriver
url=jdbc:oracle:thin:@127.0.0.1:1521:orcl
username=scott
password=tiger
initialSize=[连接池启动时创建的初始化连接数量(默认值为0)]
maxActive=[连接池中可同时连接的最大的连接数(默认值为8)]
maxIdle=[连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限制(默认为8个)]
minIdle=[连接池中最小的空闲的连接数]
maxWait=[最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待(默认为无限,调整为60000ms,避免因线程池不够用,而导致请求被无限制挂起)]
C3P0
C3P0也是开源免费的连接池!C3P0被很多人看好!
使用示例
public static void main(String[] args) {
try {
Class.forName("oracle.jdbc.OracleDriver");
DataSource unpoolds = DataSources.unpooledDataSource("jdbc:oracle:thin:@127.0.0.1:1521:orcl",
"scott", "tiger");
DataSource poolds = DataSources.pooledDataSource(unpoolds);
Connection conn = poolds.getConnection();
System.out.println(conn);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
c3p0也可以指定配置文件,而且配置文件可以是properties,也可以是xml的。当然xml的高级一些了。
但是c3p0的配置文件名必须为c3p0-config.xml,并且必须放在类路径下。
1. <!--连接池中保留的最大连接数。默认值: 15 -->
2. <property name="maxPoolSize" value="20"/>
3. <!-- 连接池中保留的最小连接数,默认为:3-->
4. <property name="minPoolSize" value="2"/>
5. <!-- 初始化连接池中的连接数,取值应在minPoolSize与maxPoolSize之间,默认为3-->
6. <property name="initialPoolSize" value="2"/>
7.
8. <!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。默认值: 0 -->
9. <property name="maxIdleTime">60</property>
10.
11. <!-- 当连接池连接耗尽时,客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒。默认: 0 -->
12. <property name="checkoutTimeout" value="3000"/>
13.
14. <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。默认值: 3 -->
15. <property name="acquireIncrement" value="2"/>
16.
17. <!--定义在从数据库获取新连接失败后重复尝试的次数。默认值: 30 ;小于等于0表示无限次-->
18. <property name="acquireRetryAttempts" value="0"/>
19.
20. <!--重新尝试的时间间隔,默认为:1000毫秒-->
21. <property name="acquireRetryDelay" value="1000" />
22.
23. <!--关闭连接时,是否提交未提交的事务,默认为false,即关闭连接,回滚未提交的事务 -->
24. <property name="autoCommitOnClose">false</property>
25.
26. <!--c3p0将建一张名为Test的空表,并使用其自带的查询语句进行测试。如果定义了这个参数那么属性preferredTestQuery将被忽略。你不能在这张Test表上进行任何操作,它将只供c3p0测试使用。默认值: null -->
27. <property name="automaticTestTable">Test</property>
28.
29. <!--如果为false,则获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常,但是数据源仍有效保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试获取连接失败后该数据源将申明已断开并永久关闭。默认: false-->
30. <property name="breakAfterAcquireFailure">false</property>
31.
32. <!--每60秒检查所有连接池中的空闲连接。默认值: 0,不检查 -->
33. <property name="idleConnectionTestPeriod">60</property>
34. <!--c3p0全局的PreparedStatements缓存的大小。如果maxStatements与maxStatementsPerConnection均为0,则缓存不生效,只要有一个不为0,则语句的缓存就能生效。如果默认值: 0-->
35. <property name="maxStatements">100</property>
36. <!--maxStatementsPerConnection定义了连接池内单个连接所拥有的最大缓存statements数。默认值: 0 -->
37. <property name="maxStatementsPerConnection"></property>