Jade Dungeon

JDBC

基础

加载数据库驱动

方法一(不推荐):

DriverManager.registerDriver(Driver driver);

方法二:

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

注意:在实际开发中并不推荐采用registerDriver方法注册驱动。原因有二:

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

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

建立连接

Connection conn = DriverManager.getConnection(url, user, pass);

Jdbc程序中的Connection,它用于代表数据库的链接。 Connection 是数据库编程中最重要的一个对象, 客户端与数据库所有交互都是通过Connection对象完成的,这个对象的常用方法:

  • createStatement():创建向数据库发送sql的statement对象。
  • prepareStatement(sql):创建向数据库发送预编译sql的PrepareSatement对象。
  • prepareCall(sql):创建执行存储过程的callableStatement对象。
  • setAutoCommit(boolean autoCommit):设置事务是否自动提交。
  • commit():在链接上提交事务。
  • rollback():在此链接上回滚事务。

批量处理

只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL:

jdbc:mysql://ip:port/db?rewriteBatchedStatements=true

这样才会在批量执行插入的时候真正执行批量操作。

如果查看PreparedStatment类的源代码,也可以看到相关逻辑:

// 确定打开rewriteBatchedStatements
if (!this.batchHasPlainStatements && 
		this.connection.getRewriteBatchedStatements()) 
{

    if (canRewriteAsMultiValueInsertAtSqlLevel()) { // instert语句有特别处理
        return executeBatchedInserts(batchTimeout);
    }

    if (this.connection.versionMeetsMinimum(4, 1, 0) && 
				!this.batchHasPlainStatements && this.batchedArgs != null
            && this.batchedArgs.size() > 3 /* 大于3条才会批处理 */) 
		{
        return executePreparedBatchAsMultiStatement(batchTimeout);
    }
}

发送SQL的Statement

Statement st = conn.createStatement();

Jdbc程序中的Statement对象用于向数据库发送SQL语句, Statement对象常用方法:

  • executeQuery(String sql):用于向数据库发送查询语句。
  • executeUpdate(String sql):用于向数据库发送insert、update或delete语句
  • execute(String sql):用于向数据库发送任意sql语句
  • addBatch(String sql):把多条sql语句放到一个批处理中。
  • executeBatch():向数据库发送一批sql语句执行。
  • clearBatch():清空此 Statement 对象的当前 SQL 命令列表。

PreparedStatement

PreperedStatement是Statement的子类,它的实例对象可以通过调用 Connection.preparedStatement()方法获得,相对于Statement对象而言的优势:

  • 防止SQL注入:PreperedStatement可以避免SQL注入的问题。
  • 预编译SQL语句:Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement 可对SQL进行预编译,从而提高数据库的执行效率。
  • 使用占位符简化语句:并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写。
Statement st = conn.prepareStatement(sql);

从结果集中获取数据

ResultSet rs = st.excuteQuery(sql);
while(rs.next()) {
	System.out.println("id=" + rs.getObject("id"));
	System.out.println("name=" + rs.getObject("name"));
	System.out.println("password=" + rs.getObject("password"));
	System.out.println("email=" + rs.getObject("email"));
	System.out.println("birthday=" + rs.getObject("birthday"));
}

ResultSet提供了对结果集进行滚动的方法:

  • next():移动到下一行
  • Previous():移动到前一行
  • absolute(int row):移动到指定行
  • beforeFirst():移动resultSet的最前面。
  • afterLast():移动到resultSet的最后面。

ResultSet既然用于封装执行结果的,所以该对象提供了用于获取数据的get方法:

SQL类型 JDBC类型 返回类型
BIT(1) getBoolean() Boolean byte[]
BIT(10) getBytes() Boolean byte[]
TINYINT getByte() Byte
SMALLINT getSort() Sort
INT getInt() Int
BIGINT getLong() Long
CHAR VARCHAR LONGVARCHAR getString() String
TEXT(CLOB) getClob() Clob
BLOG getBlog() Blob
DATE getDate() java.sql.Date
TIME getTime() java.sql.Time
TIMESTAMP getTimestamp() java.sql.Timestamp

释放资源

rs.close();
st.close();
conn.close();

CRUD操作

insert

int num = st.executeUpdate("insert into user(….) values(…..) ");
if (num>0) System.out.println("插入成功!!!");

MySQL自增主键

st = conn.prepareStatement(
		"insert into test(name) values('aaa')", 
		Statement.RETURN_GENERATED_KEYS);
st.executeUpdate();
rs = st.getGeneratedKeys();

if (rs.next()) System.out.println(rs.getInt(1));

=== update ===

{{{class="brush:java"
int num = st.executeUpdate("update user set name='' where name=''"); 
if (num>0) System.out.println("修改成功!!!");

delete

int num = st.executeUpdate("delete from user where id=1"); 
if (num>0) System.out.println("删除成功!!!");

select

ResultSet rs = st.excuteQuery("select * from user where id=1");
while (rs.next()) { /* .... */ }

分页

MySQL分页

MySQL分页的实现语句:

Select * from table limit idx, lines
  • idx:记录开始索引位置
  • lines:取多少条记录。

步骤:

  1. 先获得需分页显示的记录总数,然后在web页面中显示页码。
  2. 根据页码,从数据库中查询相应的记录显示在web页面中。

以上两项操作通常使用Page对象进行封装。

Oracle分页

Oracle分页的实现语句:

select * from (
	select rownum r_, row_.* from (
		select * from student order by id
	) row_ where rownum <= 5              -- 起始索引位置
) where r _>= 1                         -- 结束索引位置。

批量处理

Statement和PreparedStatment都可以批量执行SQL。

不同参数类型的语句:

st = conn.createStatement();

st.addBatch("insert into user(name,password,email,birthday) " + 
	"values('kkk','123','abc@sina.com','1978-08-08')");
st.addBatch("update user set password='123456' where id=3");

st.executeBatch();  // 执行批处理
st.clearBatch();    // 执行完后清空SQL语句,否则会造成内存溢出

同类型的SQL可以用PreparedStatment:

st = conn.prepareStatement("insert into " + 
		"user(name,password,email,birthday)" + 
		" values(?,?,?,?)");

for (int i=0; i < 50000; i++){
	st.setString(1, "aaa" + i);
	st.setString(2, "123" + i);
	st.setString(3, "aaa" + i + "@sina.com");
	st.setDate(4,new Date(1980, 10, 10));

	if (i % 1000 == 0) {
		st.executeBatch();  // 执行批处理
		st.clearBatch();    // 执行完后清空SQL语句,否则会造成内存溢出
	}
}
// 如果不是1000的整数还有余下来的
st.executeBatch();  // 执行批处理
st.clearBatch();    // 执行完后清空SQL语句,否则会造成内存溢出

存储过程

以MySQL为例:

CREATE PROCEDURE demoSp (
		IN inputParam VARCHAR(255), INOUT inOutParam varchar(255) )

BEGIN
	SELECT CONCAT ( 'ZYXW---- ', inputParam ) into  inOutParam ;
END
st = conn.prepareCall("{call demoSp(?,?)}");
st.setString(1, "aaaaa");
st.registerOutParameter(2, Types.VARCHAR);
st.execute();

System.out.println(st.getString(2)); 

BLOB和CLOB

MySQL大对象处理

保存CLOB:

File file = new File("src/1.txt");
FileReader reader = new FileReader(file);

PreparedStatement st = conn.prepareStatement(
		"insert into testclob(id,resume) values(?,?)");
st.setString(1, "1");
st.setCharacterStream(2, reader, (int) file.length()); // 长度须设置为int型
int num = st.executeUpdate();

if (num > 0) System.out.println("插入成功!!");

读取CLOB:

st = conn.prepareStatement(
		"select id,resume from testclob where id='1'");
rs = st.executeQuery();
if (rs.next()){
	Reader reader = rs.getCharacterStream("resume");
	FileWriter writer = new FileWriter("c:\\1.txt");
	try {
		int len = 0;
		char buffer[] = new char[1024];
		while ((len = reader.read(buffer)) > 0) writer.write(buffer, 0, len);
	} finally {
		if (reader != null) reader.close();
		if (writer != null) writer.close();
	}
}

保存BLOB:

File file = new File("src/1.jpg");
FileInputStream in = new FileInputStream(file);

st = conn.prepareStatement("insert into testblob(id,image) values(?,?)");
st.setString(1, "1");
st.setBinaryStream(2, in, (int) file.length());
int num = st.executeUpdate();

if (num > 0) System.out.println("插入成功!!");

读取BLOB:

st = conn.prepareStatement(
		"select id,image from testblob where id='1'");
rs = st.executeQuery();
if (rs.next()){
	try {
		InputStream in = rs.getBinaryStream("image");
		OutputStream out = new FileOutputStream("c:\\1.jpg");;
		int len = 0;
		byte buffer[] = new byte[1024];
		while ((len = in.read(buffer)) > 0) out.write(buffer, 0, len);
	} finally {
		if (in  != null) in.close();
		if (out != null) out.close();
	}
}

Oracle大对象处理

Oracle定义了一个BLOB字段用于保存二进制数据,但这个字段并不能存放真正的二进制数据, 只能向这个字段存一个指针,然后把数据放到指针所指向的Oracle的LOB段中,LOB段是在数据库内部表的一部分。

因而在操作Oracle的Blob之前,必须获得指针(定位器)才能进行Blob数据的读取和写入。

如何获得表中的Blob指针呢? 可以先使用insert语句向表中插入一个空的blob (调用oracle的函数empty_blob()),这将创建一个blob的指针, 然后再把这个empty的blob的指针查询出来,这样就可得到BLOB对象,从而读写blob数据了。

事务

隔离级别

set   transaction isolation level  -- 设置事务隔离级别

select @@tx_isolation              -- 查询当前事务隔离级别
  1. Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
  2. Read committed:可避免脏读情况发生(读已提交)。
  3. Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)
  4. Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化,近似于单线程操作。)

脏读

脏读: 指一个事务读取了另外一个事务未提交的数据。【针对同一个业务操作】

这是非常危险的,假设A向B转帐货款100元,对应sql语句如下所示

update account set money=money+100 while name='b';  
update account set money=money-100 while name='a';

当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,B就会损失100元。

演示脏读发生。

a窗口

set transaction isolation level read uncommitted;

start transaction;

select * from account; 
-- 发现a帐户是1000元,转到b窗口

b窗口

start transaction;

update account set money=money+100 where name='aaa';
-- 事务在不提交的情况下,转到a窗口进行查询 

a窗口

select * from account;
-- 发现a帐户是1100元,发生了脏读(这个事务读取到了别的事务未提交的数据) 

不可重复读

不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。【针对同一行数据】

不可重复读和脏读的区别:脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。

例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。

很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。

避免脏读,并演示不可重复读问题的发生。

a窗口

set transaction isolation level read committed;
start transaction;

select * from account;
-- 发现a帐户是1000元,转到b窗口

b窗口

start transaction;
update account set money=money+100 where name='aaa';
-- 事务在不提交的情况下,转到a窗口进行查询

a窗口

select * from account;
-- 发现a帐户是1000元,这时我们发现read committed这种级别可以避免脏读
-- 转到b窗口

b窗口

commit;
-- 转到a窗口

a窗口

select * from account;
-- 发现a帐户是1100元,这时就发生不可重复读(指这个事务读取到了别的事务提交的数据)

虚读(幻读)

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

如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。

演示虚读问题的发生

set transaction isolation level repeatable read;
start transaction;

select * from account;
-- 发现a帐户是1000元,并且表的总纪录数是3条,这时转到b窗口

b窗口

start transaction;

update account set money=money+100 where name='aaa';
-- 转到a窗口

a窗口

select * from account
-- 发现a帐户是1000元,这说明repeatable read这种级别可避免脏读
-- 转到b窗口

b窗口

commit;
-- 转到a窗口

a窗口

select * from account
-- 发现a帐户是1000元,这说明repeatable read这种级别还可以避免不可重复读
-- 转到b窗口

b窗口

start transaction;

insert into account(name,money) values('ddd','1000');
commit;
-- 转到a窗口 

a窗口

select * from account
-- 发现表中可能会多出一条ddd的记录,这就发生了虚读,
-- 也就是在这个事务内读取了别的事务插入的数据(幻影数据)

事务操作

Jdbc程序向数据库获得一个Connection对象时, 默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。

JDBC控制事务语句

  • Connection.setAutoCommit(false)
  • Connection.rollback()
  • Connection.commit()

设置事务回滚点

  • Savepoint sp = conn.setSavepoint()
  • Conn.rollback(sp)会滚到指定的位置,该位置之前的操作任然有效执行,因为需要被提交
  • Conn.commit()回滚后必须要提交
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;

Savepoint sp = null;

try {
	conn = DriverManager.getConnection(url, user, pass);
	conn.setAutoCommit(false);          //start transaction;

	st = conn.prepareStatement(
			"update account set money=money-100 where name='aaa'");
	st.executeUpdate();
	sp = conn.setSavepoint();

	st = conn.prepareStatement(
			"update account set money=money+100 where name='bbb'");
	st.executeUpdate();
	int x = 1/0;

	st = conn.prepareStatement(
			"update account set money=money+100 where name='ccc'");
	st.executeUpdate();

	conn.commit();
} catch (Exception e) {
	e.printStackTrace();
	conn.rollback(sp);
	conn.commit();  //手动回滚后,一定要记得提交事务
} finally {
	rs.close();
	st.close();
	conn.close();
}

事务隔离级别例子:

Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;

Savepoint sp = null;

try {
	conn = DriverManager.getConnection(url, user, pass);
	conn.setAutoCommit(false);
	// 设置事务级别
	conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

	st = conn.prepareStatement("select * from account");
	
	// ......

	conn.commit();
} finally {
	rs.close();
	st.close();
	conn.close();
}

常用模式

常用的事务管理模式:

  • 跨层传递方法参数——在Service层创建开启事务的连接,并传递到Dao层,最后在Service层提交事务;
  • ThreadLocal 绑定连接——使用ThreadLocal进行事务管理——ThreadLocal可以实现在线程范围内实现数据共享。
  • 使用Spring进行事务管理;(其实Spring内部也是用的ThreadLocal)

例子:使用ThreadLocal把连接和事务绑定到当前线程

	public class JdbcUtils {
		private static DataSource ds;

		// 为保证各层的类所使用的ThreadLocal是同一个,建议将其设定成静态的,
		// 但是一定要记得使用后要移出绑定在上面的对象。
		private static ThreadLocal<Connection> tl = 
			new ThreadLocal<Connection>();  // 其实就是一个Map集合

		static{
			try{
				Properties prop = new Properties();
				InputStream in = JdbcUtils.class.getClassLoader()
					.getResourceAsStream("dbcpconfig.properties");
				prop.load(in);

				BasicDataSourceFactory factory = new BasicDataSourceFactory();
				ds = factory.createDataSource(prop);
			} catch (Exception e) {
				throw new ExceptionInInitializerError(e);
			}
		}

		public static DataSource getDataSource() { return ds; } 

		// 备注:该获取连接的方法,仅当使用ThreadLocal来管理事务连接的情况,
		// 因为向静态对象ThreadLocal中绑定了对象,
		// 所以当我们不需要管理事务的普通获取连接的方法,就不要用此方法。
		// 应该用普通的获取连接的方法。
		public static Connection getConnection() throws SQLException{
			try{
				//得到当前线程上绑定的连接
				Connection conn = tl.get();
				if(conn==null){  //代表线程上没有绑定连接
					conn = ds.getConnection();
					tl.set(conn);
				}
				return conn;
			} catch (Exception e) { throw new RuntimeException(e); }
		} 

		public static void startTransaction(){
			try{
				//得到当前线程上绑定连接开启事务
				Connection conn = tl.get();
				if(conn==null){  //代表线程上没有绑定连接
					conn = ds.getConnection();
					tl.set(conn);
				}
				conn.setAutoCommit(false);
			} catch (Exception e) { throw new RuntimeException(e); }
		} 

		public static void commitTransaction(){
			try{
				Connection conn = tl.get();
				if(conn!=null) { conn.commit(); }
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}

		public static void closeConnection(){
			try{
				Connection conn = tl.get();
				if(conn!=null){ conn.close(); }
			}catch (Exception e) { throw new RuntimeException(e);
			}finally{
				//千万注意,解除当前线程上绑定的链接
				//(从threadlocal容器中移除对应当前线程的链接)
				tl.remove();
			}
		}

	}

采用 ThreadLocal 绑定连接 来管理事务的 Service层的代码。

public class BusinessService {  

	//用上ThreadLocal的事务管理
	public void transfer2(int sourceid,int targetid,double money) 
		throws SQLException
	{
		try{
			JdbcUtils.startTransaction();
			AccountDao dao = new AccountDao();
			Account a = dao.find(sourceid);   //select
			Account b = dao.find(targetid);   //select
			a.setMoney(a.getMoney()-money);  
			b.setMoney(b.getMoney()+money);   
			dao.update(a); //update
			dao.update(b);//update
			JdbcUtils.commitTransaction();
		} finally {
			JdbcUtils.closeConnection();
		}
	}
}