J2EE学习的一部分--JDBC详细说明

今天是关于我们JDBC相关知识,左右JDBC我想大家都很熟悉的,我记得在很早以前就开始使用它,我记得那是一个大二的学生做课程设计。但随后以完成任务,所以遇到的问题google,当时没有时间组织,下关于JDBC的知识

摘要:

JDBC(Java Data Base Connectivity,java数据库连接),由一些接口和类构成的API。
J2SE的一部分,由java.sql,javax.sql包组成。

应用程序、JDBC API、数据库驱动及数据库之间的关系



JDBC的使用步骤

1.注冊驱动 (仅仅做一次)

方式一:Class.forName(“com.mysql.jdbc.Driver”);
推荐这样的方式,不会对详细的驱动类产生依赖。
方式二:DriverManager.registerDriver(com.mysql.jdbc.Driver);
会造成DriverManager中产生两个一样的驱动,并会对详细的驱动类产生依赖。
方式三:System.setProperty(“jdbc.drivers”, “driver1:driver2”);
尽管不会对详细的驱动类产生依赖;但注冊不太方便,所以非常少使用。 
驱动类型(四种类型)


2.建立连接(Connection) 

Connection conn = DriverManager.getConnection(url, user, password);
url格式:
JDBC:子协议:子名称//主机名:port/数据库名?属性名=属性值&…
User,password能够用“属性名=属性值”方式告诉数据库。
其它參数如:useUnicode=true&characterEncoding=GBK。


3.创建运行SQL的语句(Statement)

Statement
Statement st = conn.createStatement();
st.executeQuery(sql);
PreparedStatement
String sql = “select * from table_name where col_name=?”;
PreparedStatement ps = conn.preparedStatement(sql);
ps.setString(1, “col_value”);
ps.executeQuery();


4.处理运行结果(ResultSet)

ResultSet rs = statement.executeQuery(sql);
While(rs.next()){
rs.getString(“col_name”);
rs.getInt(“col_name”);
//…
}


5.释放资源

释放ResultSet, Statement,Connection.
数据库连接(Connection)是很稀有的资源,用完后必须立即释放。假设Connection不能及时正确的关闭将导致系统宕机。

Connection的使用原则是尽量晚创建,尽量早的释放。


以下来看一下完整的Demo:

工具类:JdbcUtils

package com.weijia.firstdemo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

public class JdbcUtils {
	
	private static String user = "root";
	private static String password = "123456";
	private static String dbName = "test";
	private static  String url = "jdbc:mysql://localhost:3306/"+dbName+"?user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312";
	
	private static DataSource dataSource = null;
	
	/**
	 * 载入驱动
	 */
	static{
		try{
			Class.forName("com.mysql.jdbc.Driver");
		}catch(Exception e){
			System.out.println("Exception:"+e.getMessage()+"");
			throw new ExceptionInInitializerError(e);
		}
	}
	
	private JdbcUtils(){
	}
	
	/**
	 * 获取连接
	 * @return
	 * @throws SQLException
	 */
	public static Connection getConnection() throws SQLException{
		return DriverManager.getConnection(url);
	}
	
	public static DataSource getDataSource(){
		return dataSource;
	}
	
	/**
	 * 释放资源
	 * @param rs
	 * @param st
	 * @param conn
	 */
	public static void free(ResultSet rs,Statement st,Connection conn){
		try{
			if(rs != null){
				rs.close();
			}
		}catch(SQLException e){
			e.printStackTrace();
		}finally{
			try{
				if(st != null){
					st.close();
				}
			}catch(SQLException e){
				e.printStackTrace();
			}finally{
				try{
					if(conn != null){
						conn.close();
					}
				}catch(SQLException e){
					e.printStackTrace();
				}
			}
		}
			
	}

}


測试类:

package com.weijia.firstdemo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Demo {
	
	public static void main(String[] args) throws Exception{
		//測试代码:
		test();
		//标准规范代码:
		template();
	}
	
	//模板代码
	public static void template(){
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try {
			conn = JdbcUtils.getConnection();
			//创建语句
			st = conn.createStatement();
			//运行语句
			rs = st.executeQuery("select * from user");
			//处理结果
			while(rs.next()){
				System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t" + rs.getObject(3) + "\t");
			}
		}catch(SQLException e){
			e.printStackTrace();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs, st, conn);
		}
	}
	
	//測试
	static void test() throws Exception{
		//注冊驱动
		DriverManager.registerDriver(new com.mysql.jdbc.Driver());
		//通过系统属性来注冊驱动
		System.setProperty("jdbc.drivers","");
		//静态载入驱动
		Class.forName("com.mysql.jdbc.Driver");
		
		//建立连接
		String url = "jdbc:mysql://localhost:3306";
		String userName = "root";
		String password = "";
		Connection conn = DriverManager.getConnection(url,userName,password);
		
		//创建语句
		Statement st = conn.createStatement();
		
		//运行语句
		ResultSet rs = st.executeQuery("select * from user");
		
		//处理结果
		while(rs.next()){
			System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t" + rs.getObject(3) + "\t");
		}
		
		//释放资源
		rs.close();
		st.close();
		conn.close();
	}

}

注意:这里还要记住引入额外的jar.这个网上非常多的。这里使用的是MySql,搜一下MySql驱动的jar即可了。

这里我们将一些操作都放到一个工具类中。这样的方式是非常优雅的。


使用JDBC来实现CRUD的操作

我们这里就採用分层操作:Dao层。Service层

首先看一下domain域中的User实体

package com.weijia.domain;

import java.util.Date;

public class User {
	
	private int id;
	private String name;
	private Date birthday;
	private float money;
	
	public User(){
		
	}
	
	public User(int id,String name,Date birthday,float money){
		this.id = id;
		this.name = name;
		this.birthday = birthday;
		this.money = money;
	}
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Date getBirthday() {
		return birthday;
	}
	public void setBirthday(Date birthday) {
		this.birthday = birthday;
	}
	public float getMoney() {
		return money;
	}
	public void setMoney(float money) {
		this.money = money;
	}

	@Override
	public String toString(){
		return "[id="+id+",name="+name+",birthday="+birthday+",money="+money+"]";
	}
}

再来看一下Dao层结构:

接口:

package com.weijia.domain;

public interface UserDao {

	//加入用户
	public void addUser(User user);
	//通过userid查询用户,id是唯一的,所以返回的是一个user
	public User getUserById(int userId);
	//更新用户信息
	public int update(User user);
	//删除用户信息
	public int delete(User user);
	
}

实现类:

package com.weijia.domain;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import com.weijia.firstdemo.JdbcUtils;

public class UserDaoImpl implements UserDao{

	/**
	 * 加入用户
	 */
	public void addUser(User user) {
		Connection conn = null;
		PreparedStatement st = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)";
			st = conn.prepareStatement(sql);
			st.setInt(1,user.getId());
			st.setString(2,user.getName());
			//日期格式的转换(utils.date转化成sql.date)
			st.setDate(3,new Date(user.getBirthday().getTime()));
			st.setFloat(4, user.getMoney());
			int count = st.executeUpdate();
			System.out.println("加入记录条数:"+count);
		}catch(Exception e){
			throw new DaoException(e.getMessage(),e);
		}finally{
			JdbcUtils.free(null, st, conn);
		}
	}

	/**
	 * 删除用户
	 */
	public int delete(User user) {
		Connection conn = null;
		PreparedStatement st = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "delete from user where id=?

"; st = conn.prepareStatement(sql); st.setInt(1,user.getId()); int count = -1; count = st.executeUpdate(); System.out.println("删除记录条数:"+count); return count; }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(null, st, conn); } } /** * 通过userId获取用户信息 */ public User getUserById(int userId) { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); String sql = "select * from user where id=?"; st = conn.prepareStatement(sql); st.setInt(1,userId); rs = st.executeQuery(); if(rs.next()){ User user = new User(); user.setId(userId); user.setName(rs.getString("name")); user.setBirthday(rs.getDate("birthday")); user.setMoney(rs.getFloat("money")); return user; } }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(rs, st, conn); } return null; } /** * 更新用户信息 */ public int update(User user){ Connection conn = null; PreparedStatement st = null; try{ conn = JdbcUtils.getConnection(); String sql = "update user set name=?

,birthday=?,money=? where id=?

"; st = conn.prepareStatement(sql); st.setString(1,user.getName()); st.setDate(2,new Date(user.getBirthday().getTime())); st.setFloat(3,user.getMoney()); st.setInt(3,user.getId()); int count = 0; count = st.executeUpdate(); System.out.println("更新的记录数:"+count); return count; }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(null, st, conn); } } }


然后是Servic层:

package com.weijia.domain;

public class UserService {
	
	private UserDao userDao;
	
	public UserService(){
		//通过工厂实例化UserDao对象
		userDao = DaoFactory.getInstance().createUserDao();
		System.out.println("userDao:"+userDao);
	}
	
	/**
	 * 注冊用户
	 * @param user
	 */
	public void regist(User user){
		if(user == null){
			System.out.println("注冊信息无效!!");
		}else{
			userDao.addUser(user);
		}
		
	}
	
	/**
	 * 查询用户
	 * @param userId
	 * @return
	 */
	public User query(int userId){
		User user = userDao.getUserById(userId);
		if(user == null){
			System.out.println("查询结果为空!!");
		}else{
			System.out.println(user.getId()+"\t"+user.getName()+"\t"+user.getBirthday()+"\t"+user.getMoney());
		}
		return userDao.getUserById(userId);
	}
	
	/**
	 * 更新用户
	 * @param user
	 */
	public void update(User user){
		if(user.getId()<=0){
			System.out.println("用户id无效,无法更新");
		}else{
			userDao.update(user);
		}
	}
	
	/**
	 * 删除用户
	 * @param user
	 */
	public void delete(User user){
		if(user.getId()<=0){
			System.out.println("用户id无效,无法删除!!");
		}else{
			userDao.delete(user);
		}
	}
	
}


这里我们还须要额外的两个类:

一个是异常类,由于我们须要自己定义我们自己的一个异常,这样方便进行捕获:

package com.weijia.domain;

public class DaoException extends RuntimeException{

	private static final long serialVersionUID = 1L;
	
	public DaoException(){
		
	}
	
	public DaoException(Exception e){
		super(e);
	}
	
	public DaoException(String msg){
		super(msg);
	}
	
	public DaoException(String msg,Exception e){
		super(msg,e);
	}

}


同一时候,我们这里面採用工厂模式进行实例化UserDao对象:

package com.weijia.domain;

import java.io.FileInputStream;
import java.util.Properties;

public class DaoFactory {
	/**
	 * 单例模式
	 */
	private static UserDao userDao = null;
	private static DaoFactory instance = new DaoFactory();
	
	private DaoFactory(){
		/**
		 * 通过读取属性文件来动态的载入Dao层类
		 */
		Properties prop = new Properties();
		try{
			FileInputStream fis = new FileInputStream("src/com/weijia/domain/daoconfig.properties");
			prop.load(fis);
			String className = prop.getProperty("userDaoClass");
			Class<?

> clazz = Class.forName(className); userDao = (UserDao)clazz.newInstance(); fis.close(); }catch(Throwable e){ throw new ExceptionInInitializerError(e); } } public static DaoFactory getInstance(){ return instance; } public UserDao createUserDao(){ return userDao; } }

这里面是读取properties文件。然后去读取类名进行载入,这样的方式是非常灵活的


測试:

package com.weijia.domain;

import java.util.Date;

public class TestDemo {
	
	public static void main(String[] args) throws Exception{
		UserService userService = new UserService();
		System.out.println("加入用户:");
		userService.regist(new User(1,"jiangwei",new Date(System.currentTimeMillis()),300));
	}

}

这里我们看到事实上这些操作真的非常easy,就是依照那样的几个步骤来操作就可以,同一时候我们还须要将结构进行分层,以便管理,我们这里面測试的时候,撇开了创建数据库的一个环节。至于那个环节。也是不难的。能够从网上搜索一下就可以。


Statement中的sql依赖注入的问题

接着来看一下关于我们上面的样例中使用了Statement进行操作的。事实上这里面是存在一个问题的。就是会有sql注入的问题,我们先来看一下这个问题:

查询学生信息:

/**
	 * 使用Statement读取数据
	 * @param name
	 * @throws SQLException
	 */
	static void read(String name) throws SQLException{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try {
			conn = JdbcUtils.getConnection();
			//创建语句
			st = conn.createStatement();
			//运行语句(不建议使用*)
			String sql = "select id,name from user where name='"+name+"'";
			rs = st.executeQuery(sql);
			//依据列名取数据
			while(rs.next()){
				System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t");
			}
		}catch(SQLException e){
			e.printStackTrace();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs, st, conn);
		}
	}

我们使用代码測试一下:

read("'or 1 or'");

我们执行会发现,将查询出全部的学生的记录,这个是什么原因呢?我们最好还是将sql打印一下会发现:

select id,name from user where name=''or 1 or''

擦,由于sql语句中把1觉得是true,又由于是或的关系,所以将全部的学生的信息查询出来了,这个就是sql注入,由于Statement会把传递进来的參数进行一下转化操作。用引號包括一下。所以会出现这个问题。那么我们该怎么解决呢?有的同学说我们能够加入一句过滤的代码,将传递的參数取出单引號,这种方法是可行的的,可是这个仅仅能解决那些使用单引號的数据库。可能有的数据库使用的是双引號包括内容。那就不行了,所以应该想一个全套的方法。那么这里我们就是用一个叫做:PreparedStatement类。这个类是Statement类的子类,关于这两个类的差别能够查看我的另外一片文章:

http://blog.csdn.net/jiangwei0910410003/article/details/26143977

我们这里仅仅看这个sql注入的问题:

我们将上面读取用户信息的代码改写成PreparedStatement:

/**
	 * 使用PreparedStatement
	 * @param name
	 * @throws SQLException
	 */
	static void readPrepared(String name) throws SQLException{
		Connection conn = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			//运行语句(不建议使用*)
			String sql = "select id,name from user where name=?

"; //创建语句 st = conn.prepareStatement(sql); st.setString(1, name); rs = st.executeQuery(); //依据列名取数据 while(rs.next()){ System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t"); } }catch(Exception e){ } }


之后我们在运行:

readPrepared("'or 1 or'");

就不会所有查出来了。仅仅会查询空结果,由于表中没有一个学生的名字叫做 'or 1 or'。


JDBC中特殊数据类型的操作问题

第一个是日期问题:

我们在操作日期问题的时候会发现,使用PreparedStatement进行參数赋值的时候,有一个方法是:setDate(...)。可是这种方法接收的參数是sql中的Date类,而不是我们寻常使用的util中的Date类。所以我们要做一次转化,通常我们是这样做的。就是在定义实体类的时候将其日期型的属性定义成util中的Date类型,在进行数据库操作的时候.

进行一次转换:setDate(x,new Date(birthday.getTime());,这里birthday就是一个util.Date类型的一个属性,而new Date是sql.Date类型的。这样转化就能够了,相同我们在读取数据的时候将转化操作反过来就可以。


第二个问题就是大文本数据的问题

由于有时候我们会存入一些文本内容。由于varchar的大小在mysql中也是有上线的。所以我们这里要使用blob类型了,我们这里来看一下实例:

/**
	 * 插入大文本
	 */
	static void insert(){
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert into clob_test(bit_text) values(?)";
			ps = conn.prepareStatement(sql);
			File file = new File("src/com/weijia/type/ClubDemo.java");
			Reader reader = new BufferedReader(new FileReader(file));
			//ps.setAsciiStream(1, new FileInputStream(file), (int)file.length());//英文的文档
			ps.setCharacterStream(1, reader, (int)file.length());
			ps.executeUpdate();
			reader.close();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,ps,conn);
		}
	}


我们将一个Java代码文件插入到数据库中

我们查询一下clob_test表:


我们看到文件内容存入到库中了。相同我们也能够从表中读取一段文本出来,使用

Clob clob = rs.getClob(1);
InputStream is = clob.getAsciiStream();

或者读取一个Reader也是能够的,这里的InputStream和Reader是针对不同流,一个字节流,这个不须要关心编码问题的,Reader是字符流须要关心编码问题。


JDBC中事务的概念

我们当初在学习数据库的时候就了解事务的概念了,事务在数据库中的地位是非常重要的。

在JDBC中默认情况事务是自己主动提交的,所以我们在进行CRUD操作的时候不须要关心开启事务,提交事务,事务回滚的一些操作,那么以下我们就来看一下怎么手动的操作一些事务:

下载我们假定这种一个场景:

有来两个用户1和2,如今

将用户1中的账户的钱降低10
查询用户2中的账户的钱,假设钱少于300,就添加10,否则抛出异常

看一下代码:

static void test() throws Exception{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			/**************事务START********************/
			conn.setAutoCommit(false);
			st = conn.createStatement();
			
			String sql = "update user set money=money-10 where id=1";
			st.executeUpdate(sql);
			
			sql = "select money from user where id=2";
			rs = st.executeQuery(sql);
			float money = 0.0f;
			if(rs.next()){
				money = rs.getFloat("money");
			}
			if(money>300){
				throw new RuntimeException("已经超过最大值");
			}
			sql = "update user set money=money+10 where id=2";
			st.executeUpdate(sql);
			conn.commit();
			/*******************事务END*********************/
		}catch(RuntimeException e){
			
		}finally{
			JdbcUtils.free(rs, st, conn);
		}
	}

我们执行測试一下,由于我们这里想让它抛出异常,所以我们将用户2中的钱改成大于300的。执行一下,结果抛出异常了。可是我们发现了用户1中的钱少了10,可是由于抛出异常,所以后面的代码不执行了,用户2中的钱没有变化。那么这种操作明显不正确的,所以我们这时候要解决问题,使用事务的回滚操作,在捕获到异常的时候须要做回滚操作:

if(conn != null){
   conn.rollback();
}

这样即使抛出了异常,这些操作也会进行回滚的,那么用户1中的钱就不会少10了。

同一时候上面我们看到,我们是在開始的时候手动的关闭事务的自己主动提交,然后再手动的提交事务,以下再来看一下事务的保存点的问题。

场景:在上面的基础上。我们加入一个用户3,同一时候对用户1和用户3中的钱进行降低10,用户2的操作不变,可是当抛出异常的时候,我们希望用户1的操作还是有效的,用户3的操作还原,这时候我们须要将事务回滚到用户3的那个点就能够了,这就是事务的保存点的概念,看一下代码:

static void test() throws Exception{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		Savepoint sp = null;
		try{
			conn = JdbcUtils.getConnection();
			/**************事务START********************/
			conn.setAutoCommit(false);
			st = conn.createStatement();
			
			String sql = "update user set money=money-10 where id=1";
			st.executeUpdate(sql);
			sp = conn.setSavepoint();//设置回滚点
			
			sql = "update user set money=money-10 where id=3";
			st.executeUpdate(sql);
			
			sql = "select money from user where id=2";
			rs = st.executeQuery(sql);
			float money = 0.0f;
			if(rs.next()){
				money = rs.getFloat("money");
			}
			System.out.println("money:"+money);
			if(money>300){
				throw new RuntimeException("已经超过最大值");
			}
			sql = "update user set money=money+10 where id=2";
			st.executeUpdate(sql);
			conn.commit();
			/*******************事务END*********************/
		}catch(SQLException e){
			if(conn != null && sp != null){
				conn.rollback(sp);
				conn.commit();
			}
		}finally{
			JdbcUtils.free(rs, st, conn);
		}
	}

我们在用户1之后设置一个保存点,在异常中仅仅须要回滚到保存点就能够了。


以下再来看一下事务的隔离级别,由于这部分的内容比較重要和繁琐。请看另外一篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/24960785

JDBC中调用存储过程

关于MySql中的存储过程的知识请看这两篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/24964331

http://blog.csdn.net/jiangwei0910410003/article/details/24965087

当我们会创建存储过程的时候,我们在到JDBC中去调用这个存储过程。

以下看一下实例:

我们如今想在插入一条数据的时候可以得到主键id的值(由于我们一般把主键id的值设置成自增长的形式),首先来创建一个存储过程:

delimiter $$ //改动定界符
drop procedure if exists addUser $$
create procedure addUser(in name varchar(45),in birthday date,in money float,out pid int)
begin
 insert into user(name,birthday,money) values(anme,birthday,money);
 select last_insert_id() into pid;//当前线程拿到最后一次插入的记录的赋值给pid,这里要注意,user表中的id必须是主键自增长类型,不然报错
end $$
delimiter ;

这里name,birthday,money都是输入值是:in

pid是输出值:out

然后我们在代码中进行运行这个存储过程:

static void test() throws Exception{
		Connection conn = null;
		CallableStatement cs = null;
		try{
			conn = JdbcUtils.getConnection();
			//name,birthday,money,id
			//存储过程名称是:addUser
			String sql = "{ call addUser(?

,?

,?,?)}"; cs = conn.prepareCall(sql); cs.registerOutParameter(4, Types.INTEGER); cs.setString(1,"jiangwei"); cs.setDate(2,new Date(System.currentTimeMillis())); cs.setFloat(3,300); cs.executeUpdate(); int id = cs.getInt(4); System.out.println("id:"+id); /** * 通过这个存储过程来获取主键id是有一个问题,不同的数据库,存储过程的编写语法是不一样的,所以这样的方法是不通用 * 还有第二种方法是OtherApi,通过JDBC中的api来获取 */ }catch(SQLException e){ e.printStackTrace(); } }

这样我们就得到了插入一条记录的时候得到他的主键id的值

事实上这样的调用存储结构的方法。在早起的时候是非常有用的。由于那时候没有分层架构的思想。所以会将业务逻辑层的实现放到存储过程中去做了,在调用存储过程的时候,会发现一个问题就是这样去获取主键id的值的方式,是不通用的,由于不同的数据库可能存储过程的编写是不一样的,所以做不到一致性,并且如今有了三成架构的思想。我们慢慢的就将这样的方式给淘汰了,而是直接使用JDBC给我们提供的一套api来获取主键key的值:直接上代码吧:

static void test() throws Exception{
		java.sql.Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert into user(name,birthday,money) values('jiangwei','1987-01-01',400)";
			ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);//生成主键id
			ps.executeUpdate();
			//可能是组合主键,可能会返回一个ResultSet
			rs = ps.getGeneratedKeys();
			int id = 0;
			if(rs.next()){
				id = rs.getInt(1);
			}
			System.out.println("id:"+id);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,ps,conn);
		}
	}

我们仅仅要设置一个參数Statement.RETURN_GENERATED_KEYS就能够得到一个主键集合了,这里要注意的是。由于有的表结构中会出现组合主键的情况,所以返回的是一个主键集合。

这样的方式就和底层数据库摆脱了关系。做到一致性了。


JDBC来实现批处理功能

我们在前面的样例中会发现,每次都是运行一条语句。然后关闭连接。这样效率可能会非常低。假设我们想一次插入几千条数据的话,这时候能够使用批处理的功能,所谓批处理就是将多个运行语句进行捆绑然后去运行,可是效率上并不是就一定高,由于我们知道这个数据库连接是tcp的。所以在将多个语句捆绑在一起的时候,在传输的过程中也是会进行分包发送的,这个包的大小也不是固定的。这个大小非常难掌控的,我们之后经过多次測试之后。才干得到一次批量处理的适宜数量。以下来看一下实例吧:

首先是普通的插入一条数据:

static void create() throws Exception{
		//建立一个连接的是非常耗时间的
		//运行一个sql语句也是非常耗时间的
		//优化的措施:批处理
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert user(name,birthday,money) values(?,?

,?

)"; ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS); ps.setString(1,"jiangwei"); ps.setDate(2,new Date(System.currentTimeMillis())); ps.setFloat(3,400); ps.executeUpdate(); }catch(Exception e){ e.printStackTrace(); }finally{ JdbcUtils.free(rs, ps, conn); } }


然后是批处理插入100条数据:

static void createBatch() throws Exception{
		//建立一个连接的是非常耗时间的
		//运行一个sql语句也是非常耗时间的
		//优化的措施:批处理
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			String sql = "insert user(name,birthday,money) values(?

,?,?)"; ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS); //打包的话容量也不是越大越好,由于可能会内存溢出的,同一时候网络传输的过程中也是会进行拆包传输的。这个包的大小是不一定的 //有时候打包的效率不一定就会高。这个和数据库的类型。版本号都有关系的,所以我们在实践的过程中须要检验的 for(int i=0;i<100;i++){ ps.setString(1,"jiangwei"); ps.setDate(2,new Date(System.currentTimeMillis())); ps.setFloat(3,400); //ps.addBatch(sql); ps.addBatch(); } ps.executeBatch(); }catch(Exception e){ e.printStackTrace(); }finally{ JdbcUtils.free(rs, ps, conn); } }


測试代码:

public static void main(String[] args) throws Exception{
		long startTime = System.currentTimeMillis();
		for(int i=0;i<100;i++){
			create();
		}
		long endTime = System.currentTimeMillis();
		System.out.println("For Waste Time:"+(endTime-startTime));
		createBatch();
		System.out.println("Batch Waste Time:"+(System.currentTimeMillis()-endTime));
	}

我们在控制台中看到他们分别消耗的时间:


我们能够看到这个批处理消耗的时间明显非常少。。

当然我们在開始的时候也说过了。这个批处理的最适宜的大小要掌控好。


JDBC中的滚动结果集和分页技术

我们在前面的样例中能够看到。在处理结果集的时候,我们都是一条一条向后处理的,可是有时候我们须要人为的控制结果集的滚动,比方我们想往前滚动,想直接定位到哪个结果集记录等操作。当然JDBC也是提供了一套Api让我们来操作的

static void test() throws Exception{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			//结果集可滚动的
			/**
			 * 參数的含义:
			 *  ResultSet.RTYPE_FORWORD_ONLY:这是缺省值,仅仅可向前滚动。 
				ResultSet.TYPE_SCROLL_INSENSITIVE:双向滚动,但不及时更新,就是假设数据库里的数据改动过,并不在ResultSet中反应出来。 
				ResultSet.TYPE_SCROLL_SENSITIVE:双向滚动,并及时跟踪数据库的更新,以便更改ResultSet中的数据。
				ResultSet.CONCUR_READ_ONLY:这是缺省值,指定不能够更新 ResultSet 
				ResultSet.CONCUR_UPDATABLE:指定能够更新 ResultSet
			 */
			st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_READ_ONLY);
			rs = st.executeQuery("select id,name,money,birthday from user");
			//開始的时候这个游标的位置是第一条记录之前的一个位置
			//当运行rs.next的时候这个游标的位置就到第一条记录了
			/*while(rs.next()){
				//print result
			}*/
			//上面的代码运行之后,这个游标就到最后一条记录的下一个位置了
			//所以这里在调用previous方法之后,这个游标就回到了最后一条记录中,所以打印了最后一条记录的值
			/*if(rs.previous()){
				System.out.println("id="+rs.getInt("id")+"\tname="+rs.getString("name")+"\tbirthday="+rs.getDate("birthday")+"\tmoney="+rs.getFloat("money"));
			}*/
			
			//绝对定位到第几行结果集
			//这里传递的參数的下标是从1開始的,比方这里查询出来的记录有3条,那么这里的參数的范围是:1-3,假设传递的參数不在这个范围内就会报告异常的
			rs.absolute(2);
			System.out.println("id="+rs.getInt("id")+"\tname="+rs.getString("name")+"\tbirthday="+rs.getDate("birthday")+"\tmoney="+rs.getFloat("money"));
			
			//滚到到第一行的前面(默认的就是这样的情况)
			rs.beforeFirst();
			
			//滚动到最后一行的后面
			rs.afterLast();
			
			rs.isFirst();//推断是不是在第一行记录
			rs.isLast();//推断是不是在最后一行记录
			rs.isAfterLast();//推断是不是第一行前面的位置
			rs.isBeforeFirst();//推断是不是最后一行的后面的位置
			
			//以上的api能够实现翻页的效果(这个效率非常低的,由于是先把数据都查询到内存中。然后再进行分页显示的)
			
			//效率高的话是直接使用数据库中的分页查询语句:
			//select * from user limit 150,10;
			
			//以上的api实现的分页功能是针对于那些本身不支持分页查询功能的数据库的。假设一个数据库支持分页功能,上面的代码就不能使用的,由于效率是非常低的
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,st,conn);
		}
	}

我们看到结果集:rs有非常多方法的。我们一次来看一下:

next():这个非经常常使用的,就是将结果集向后滚动
previous():这种方法和next是相反的。将结果集向前滚动
absolute(int index):这种方法是将结果集直接定位到指定的记录上,这个參数是从1開始的,不是0。假设不在指定的范围内的话,会报告异常的
beforeFirst():这种方法是将结果集直接滚动到第一条记录的前面的位置(默认情况是这种。所以我们每次在取出数据的时候,须要使用next方法。将结果集滚动到第一条记录上)
afterLast():这种方法是将结果集直接滚动到最后一条记录的后面的位置
isFirst():推断是不是在第一行记录
isLast():推断是不是在最后一行记录
isAfterLast():推断是不是第一行前面的位置
isBeforeFirst():推断是不是最后一行的后面的位置


当然我们要向实现可滚动的结果集。还要设置一下參数:

st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_READ_ONLY);

參数的含义:

ResultSet.RTYPE_FORWORD_ONLY:这是缺省值,仅仅可向前滚动。 
ResultSet.TYPE_SCROLL_INSENSITIVE:双向滚动。但不及时更新,就是假设数据库里的数据改动过,并不在ResultSet中反应出来。

 
ResultSet.TYPE_SCROLL_SENSITIVE:双向滚动,并及时跟踪数据库的更新,以便更改ResultSet中的数据。
ResultSet.CONCUR_READ_ONLY:这是缺省值,指定不能够更新 ResultSet 
ResultSet.CONCUR_UPDATABLE:指定能够更新 ResultSet(这个后面会说到)

同一时候在这里我们仅仅须要使用absolute方法就能够实现分页的功能,由于他能够随便的定位到指定的记录集中,可是这个是在所有将结果集查询处理的基础上来实现的,就是首先将所有符合条件的结果集查询出来放到内存中。然后再即可分页操作,那么这样的分页的效率就非常低了。所以我们强烈建议在数据库查询数据的时候就进行分页操作,比方MySql中使用limitkeyword进行操作,MSSQL中使用topkeyword。Oracle中使用numberkeyword,可是有的数据库中不支持分页查询操作,所以这时候我们仅仅能使用上面的形式来进行分页了。


JDBC中的可更新以及对更新敏感的结果集操作

我们有时候可能有这种需求。就是在查询出结果集的时候,想对指定的记录进行更新操作。说白了,就是将查询和更新操作放到一起进行。来看一下代码:

static void test() throws Exception{
		Connection conn = null;
		Statement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			//第三个字段的含义是,在读取数据的时候(已经返回了结果集到内存中了),
			//再去改动结果集中的数据。这时候数据库中的数据就能够感知到结果集中的变化了进行改动
			st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
			rs = st.executeQuery("select * from user");
			//这样的操作是不可取的,由于查询和更新交互在一起。逻辑就乱了,仅仅有在特定的场合中使用
			while(rs.next()){
				//这里我们获取到name列的值,假设是lisi我们就将结果集中的他的记录中的money变成170。
				//然后再更行行信息,这时候数据库中的这条记录的值也发生变化了,
				//内存中的结果集中的记录的值发生改变了,影响到了数据库中的值
				String name = rs.getString("name");
				if("jiangwei".equals(name)){
					rs.updateFloat("money",170);
					rs.updateRow();
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,st,conn);
		}
	}


我们看到在循环处理结果集的时候。我们将name是jiangwei的记录的钱改动成170。而且反映到数据库中

这里一定要记得设置參数:ResultSet.CONCUR_UPDATABLE,不然会报异常的,这个參数的功能就是将更新的操作同步到数据库中的

这里我们是不建议这样的做法的,由于将查询和更新的操作放到一起来操作的话,维护是非常差的,我们一定要将CRUD操作进行分开处理。这里仅仅是介绍一下相关知识,不推荐使用的。


以下来看一下通过反射技术,来实现将结果集填充到指定的实体类中。事实上这部分的内容非常easy的,直接上代码:

/**
	 * 使用泛型
	 * @param <T>
	 * @param sql
	 * @param clazz
	 * @return
	 * @throws Exception
	 */
	static <T> T test(String sql,Class<T> clazz) throws Exception{
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			ps = conn.prepareStatement(sql);
			rs = ps.executeQuery();
			ResultSetMetaData rsmd = rs.getMetaData();
			int count = rsmd.getColumnCount();
			String[] colNames = new String[count];
			for(int i=1;i<=count;i++){
				colNames[i-1] = rsmd.getColumnLabel(i);//使用别名。让列名和User中的属性名同样
			}
			
			T user = clazz.newInstance();
			//使用反射获取set方法来进行赋值
			if(rs.next()){
				Method[] ms = user.getClass().getMethods();
				for(int i=0;i<colNames.length;i++){
					String colName = colNames[i];
					String methodName = "set" + colName;
					for(Method method:ms){
						//通过列名来找到实体类中的属性方法(这里要注意的是set方法的格式是:setXxx首字母是大写的)
						//这里直接使用忽视大写和小写的相等的方法
						//或者使用上面的重命名来解决问题
						if(methodName.equalsIgnoreCase(method.getName())){
							method.invoke(user, rs.getObject(colNames[i]));
						}
					}
				}
			}
			return user;
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,ps,conn);
		}
		return null;
	}


測试代码:

public static void main(String[] args) throws Exception{
		//User user = test("select * from user",User.class);
		//使用别名来规定列名和属性名同样
		User user = test("select id as Id,name as Name,birthday as Birthday,money as Money from user",User.class);
		System.out.println(user);
	}

事实上就是使用反射技术将得到实体类中全部属性的set方法,然后通过set方法进行属性值的填充,这里唯一要注意的问题就是返回的结果集中的字段名称必需要和实体类中的属性名称同样,要做到这一点我们又两种方式:

一种是直接将表中的字段名称和实体类中的属性名称同样

一种是使用别名的方式来操作,将别名设置的和实体类中的属性名称同样

事实上我们会发现,后面说到的Hibernate框架就是採用这样的机制的


元数据的相关知识

我们知道元数据信息就是标示数据本身的一些数据信息

1.数据库的元数据信息

就是数据库的相关信息。如数据库的版本。驱动名称,是否支持事务操作等信息,JDBC提供了接口让我们能够获取这些信息的:

static void test() throws Exception{
		Connection conn = JdbcUtils.getConnection();
		//这些信息对于那些框架的编写就非常实用了,由于框架是要兼容各个数据库类型的,如Hibernate中有一个方言设置
		//假设没有设置的话。他就会自己使用下面的api进行查找是那个数据库
		DatabaseMetaData metaData = conn.getMetaData();
		System.out.println("databaseName:"+metaData.getDatabaseProductName());
		System.out.println("driverName:"+metaData.getDriverName());
		System.out.println("isSupportBatch:"+metaData.supportsBatchUpdates());
		System.out.println("isSupportTransaction:"+metaData.supportsTransactions());
	}

这些信息对于我们使用人员来说可能没有太大的用处,可是对于开发框架的人来说用处非常大的,比方Hibernate框架,他要做到兼容全部的数据库特性的话,必需要将不同特性统一起来。所以他肯定要获取数据库的元数据信息的,后面会说到Hibernate中有一个配置叫做:方言,这个就是用来设置数据库名称的。

2.查询參数的元数据信息

当我们在使用PreparedStatement来进行參数填充的时候,我们想知道參数的一些信息,直接上代码:

static void test(String sql,Object[] params) throws Exception{
		Connection conn = JdbcUtils.getConnection();
		//參数的元数据信息:Statement是没有參数的元数据信息的(由于Statement不支持?),查看源码。返回的都是varchar
		PreparedStatement ps = conn.prepareStatement(sql);
		//必须在连接数据库中的时候加入这个參数generateSimpleParameterMetadata=true
		//不然是获取不到參数的,并且会报异常
		ParameterMetaData pMetaData= ps.getParameterMetaData();
		int count = pMetaData.getParameterCount();
		for(int i=1;i<=count;i++){
			//由于mysql没有去查询库。所以不能依据查询的字段就能获取字段的类型,全部都返回varchar
			System.out.println(pMetaData.getParameterClassName(i));
			System.out.println(pMetaData.getParameterType(i));
			System.out.println(pMetaData.getParameterTypeName(i));
			//假定我们传入的參数的顺序和sql语句中的占位符的顺序一样的
			ps.setObject(i,params[i-1]);
		}
		ps.executeQuery();
	}


測试代码:

public static void main(String[] args) throws Exception{
		String sql = "select name,birthday,money from user where name=?";
		Object[] params = new Object[]{"jiangwei"};
		test(sql,params);
	}


我们知道Statement是不支持參数填充的。所以不可能获取到參数的元数据信息的

我们执行測试代码,会看到例如以下异常信息:


这时候我们就要注意了。假设想获取到元数据信息的话,我们还须要在连接数据的url后面加入一个參数:

generateSimpleParameterMetadata=true

加入完之后,我们执行结果例如以下:


我们看到能够获取參数在Java中的类型,12代表的是sql包中的类型:java.sql.Types.VARCHAR。这个字段是个整型值,值就是12,他相应到数据库中varchar类型的

这里要注意一点:有时候我们会发现这里获取到数据库中的类型是错误的,比方这里我们假设将数据库中的name字段的类型改动成char类型的,这里获取到的还是varchar,这一点想一想也是对的,你想想这个是获取查询參数的信息,我们还没有进行查询操作的,系统不可能那么智能的获取到数据库中准确的字段的类型的,所以他这里就做了一个大致的相应关系,将Java中的类型和数据库中的类型相应起来的,由于数据库中char和varchar都是字符串的。所以我们不能相信这里得到的数据库中字段的类型的。须要通过结果集中的元数据类型。

3.结果集中元数据信息

就是查询结果的一般信息,比方字段的相关信息

我们在上面看到要想获取数据库中字段的真实类型的话。仅仅有先进行查询操作才干够,在这里我们就能够获取到正确的类型了,上代码:

static void test(String sql) throws Exception{
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			ps = conn.prepareStatement(sql);
			rs = ps.executeQuery();
			ResultSetMetaData rsmd = rs.getMetaData();
			int count = rsmd.getColumnCount();
			String[] colNames = new String[count];
			for(int i=1;i<=count;i++){
				//这里是能够获取到真实的类型的,由于这个是已经从数据库中查询了
				System.out.println(rsmd.getColumnClassName(i));
				System.out.println(rsmd.getColumnName(i));
				System.out.println(rsmd.getColumnType(i));
				System.out.println(rsmd.getColumnLabel(i));//列的别名:select name as n from user;,有别名的话就返回的是别名。而不是原始的列名了
				colNames[i-1] = rsmd.getColumnName(i);
			}
			//将结果构建一个Map。列名是key,列的值是value
			Map<String,Object> data = null;
			//如果查询的数据仅仅有一条,如果是多条的话我们能够定义一个List<Map<...这种结构
			if(rs.next()){
				data = new HashMap<String,Object>();
				for(int i=0;i<colNames.length;i++){
					data.put(colNames[i], rs.getObject(colNames[i]));
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			JdbcUtils.free(rs,ps,conn);
		}
	}

我们这里能够获取到结果集中字段的总数count,以及字段的类型。名称,别名等信息,同一时候我们这里还穿插了一段代码,就是将结果集封装成一个HashMap结构。字段名做key,字段值做value


JDBC中的数据源

首先我们要知道什么是数据源。为什么要有数据源。我们从上面的样例看到。我们每次运行操作的时候,都是打开连接,关闭连接,这个连接的建立和关闭是非常好资源和时间的,所以我们就在想一个策略怎么才干优化呢?所以数据源的概念就出来的,数据源就是用来管理连接的一个池子,使用高效的算法进行调度,这样在运行操作的时候是非常方便的,为了easy理解数据源的相关概念。我们自己编写一个数据源:

package com.weijia.datasource;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;

import javax.sql.DataSource;

/**
 * 大部分时间都是浪费在数据库连接这一块
 * 这个类是我们自己编写的一个数据源
 * @author weijiang204321
 *
 */
public class MyDataSource implements DataSource{

	private static String user = "root";
	private static String password = "123456";
	private static String dbName = "test";
	private static  String url = "jdbc:mysql://localhost:3306/"+dbName+"?user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312";
	
	private static int initCount = 5;//初始化的连接数
	private static int maxCount = 10;//最大连接数
	private static int currentCount = 0;//当前的连接数
	//可能频繁的取出连接和删除连接,所以用LinkedList
	private LinkedList<Connection> connectionsPool = new LinkedList<Connection>();
	
	public MyDataSource(){
		try{
			for(int i=0;i<initCount;i++){
				this.connectionsPool.addLast(createConnection());
				currentCount++;
			}
		}catch(Exception e){
			throw new ExceptionInInitializerError(e);
		}
	}
	
	public Connection getConnection() throws SQLException{
		//也有可能获取不到连接。并且这种方法也是可能被多线程訪问的
		synchronized(connectionsPool){
			if(connectionsPool.size() > 0){
				return this.connectionsPool.removeFirst();
			}
			if(currentCount < maxCount){
				currentCount++;
				return createConnection();
			}
			
			//在这里能够让当前线程等待。抛出异常,返回null都是能够的,要视情况而定
			throw new SQLException("已经没有连接了");
			
			//不能无限制的创建连接的,由于这种话对数据库的压力非常大,连接越多。最后数据库的执行速度就会变得非常慢了(非常硬件相关)
			//假设内存够大,cpu给力的话。数据库能够建立的连接数也会添加的
		}
	}
	
	public void free(Connection conn) throws SQLException{
		this.connectionsPool.addLast(conn);
	}
	
	private Connection createConnection() throws SQLException{
		return DriverManager.getConnection(url);
	}

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

	public PrintWriter getLogWriter() throws SQLException {
		return null;
	}

	public int getLoginTimeout() throws SQLException {
		return 0;
	}

	public void setLogWriter(PrintWriter arg0) throws SQLException {
		
	}

	public void setLoginTimeout(int arg0) throws SQLException {
		
	}
	
}


然后改动一下JdbcUtils中的代码:

package com.weijia.firstdemo;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import com.weijia.datasource.MyDataSource;

public class JdbcUtils {
	
	private static String user = "root";
	private static String password = "123456";
	private static String dbName = "test";
	private static  String url = "jdbc:mysql://localhost:3306/"+dbName+"?user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312&generateSimpleParameterMetadata=true";
	
	private static MyDataSource dataSource = null;
	
	/**
	 * 载入驱动
	 */
	static{
		try{
			Class.forName("com.mysql.jdbc.Driver");
			dataSource = new MyDataSource();//初始化数据源
		}catch(Exception e){
			System.out.println("Exception:"+e.getMessage()+"");
			throw new ExceptionInInitializerError(e);
		}
	}
	
	private JdbcUtils(){
	}
	
	/**
	 * 获取连接
	 * @return
	 * @throws SQLException
	 */
	public static Connection getConnection() throws SQLException{
		return dataSource.getConnection();
	}
	
	public static DataSource getDataSource(){
		return dataSource;
	}
	
	/**
	 * 释放资源
	 * @param rs
	 * @param st
	 * @param conn
	 */
	public static void free(ResultSet rs,Statement st,Connection conn){
		try{
			if(rs != null){
				rs.close();
			}
		}catch(SQLException e){
			e.printStackTrace();
		}finally{
			try{
				if(st != null){
					st.close();
				}
			}catch(SQLException e){
				e.printStackTrace();
			}finally{
				try{
					dataSource.free(conn);
				}catch(SQLException e){
					e.printStackTrace();
				}
			}
		}
			
	}

}


我们看到,在我们自己定义的数据源中,主要有这么几个变量:

初始化连接数,最大连接数,当前的连接数,连接池(由于我们可能须要频繁的加入连接和删除连接所以使用LinkedList,由于这个list是链表结构的,添加和删除效率高)

主要流程是:初始化数据源的时候,初始化一定量的连接放到池子中,当用户使用getConnection()方法取出连接的时候,我们会推断这个连接池中还有没有连接了,有就直接取出第一个连接返回,没有的话。我们在推断当前的连接数有没有超过最大连接数,超过的话。就抛出一个异常(事实上这里还能够选择等待其它连接的释放,这个详细实现是非常麻烦的),没有超过的话。就创建连接。而且将其放入池子中。

我们自己定义的数据源是实现了JDBC中的DataSource接口的,这个接口非常重要的,后面我们会说到apache的数据源都是要实现这个接口的,这个接口统一了数据源的标准,这个接口中有非常多实现的。所以看到我们的数据源类中有非常多不是必需的方法,可是那个方法都是要实现的,最重要的就是要实现getConnection方法,其它的实现都仅仅须要调用super.XXX就能够了。


在JdbcUtils类中我们也是须要改动的。首先我们要在静态代码块中初始化我们的数据源。在getConnection方法中调用数据源的getConnection方法,在free方法中调用数据源的free方法就可以。


看一下測试类:

package com.weijia.datasource;

import java.sql.Connection;

import com.weijia.firstdemo.JdbcUtils;

public class Test {
	
	public static void main(String[] args) throws Exception{
		for(int i=0;i<10;i++){
			Connection conn = JdbcUtils.getConnection();
			System.out.println(conn);
			JdbcUtils.free(null, null, conn);
		}
	}

}


执行结果:


我们能够看到。我们在測试代码中申请了10个连接,从结果上能够看出前五个是不同的连接,后五个连接和前五个是一样的。这是由于我们在释放连接的时候就是free方法中。是将连接又一次放到池子中的。上面显示的是五个。是由于我们初始化的连接数是5个,当第一个连接释放的时候这个连接事实上已经放到了池子的第六个位置,以此类推。


以下我们继续来看下个问题。我们在上面的数据源中能够看到,我们定义了一个free方法来释放连接的,然后在JdbcUtils中调用这种方法就可以,可是这个貌似不太符合我们的使用习惯,由于之前我们看到我们释放连接的时候都是使用close方法的,所以这里面我们在改动一下。至于怎么改动呢?

首先我们知道那个close方法是JDBC中的Connection接口中的,全部自己定义的连接都是须要实现这个接口的,那么我们假设我们想让我们free中的逻辑放到close中的话。就须要实现这个接口了。我们能够看到

DriverManager.getConnection(url)

通过这样的方式获取到的Connection也是mysql中实现了Connection的接口的,那么如今我们可能须要自定一个我们自己的连接,然后实现Connection接口,将free方法中的逻辑搬到close方法中。同一时候我们还要在连接类中保持一个mysql中的连接对象,这里面的逻辑有点不好理解,先看代码:

package com.weijia.datasource;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.Map;

public class MyConnection implements Connection{
	
	//组合方式:静态代理
	private Connection realConnection;
	private MyDataSource2 dataSource;
	//当前连接的使用的次数
	private int maxUseCount = 5;
	private int currentUseCount = 0;
	
	public MyConnection(Connection conn,MyDataSource2 dataSource){
		this.realConnection = conn;
		this.dataSource = dataSource;
	}
	
	public void close() throws SQLException {
		this.currentUseCount++;
		if(this.currentUseCount < this.maxUseCount){
			this.dataSource.free(this);
		}else{
			dataSource.currentCount--;
			this.realConnection.close();
		}
	}

	public void clearWarnings() throws SQLException {
		this.realConnection.clearWarnings();
	}

	public void commit() throws SQLException {
		this.realConnection.commit();
	}

	public Statement createStatement() throws SQLException {
		return this.realConnection.createStatement();
	}

	public Statement createStatement(int resultSetType, int resultSetConcurrency)throws SQLException {
		return this.realConnection.createStatement(resultSetType, resultSetConcurrency);
	}

	public Statement createStatement(int resultSetType,int resultSetConcurrency, int resultSetHoldability)throws SQLException {
		return this.realConnection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
	}

	public boolean getAutoCommit() throws SQLException {
		return this.realConnection.getAutoCommit();
	}

	public String getCatalog() throws SQLException {
		return this.realConnection.getCatalog();
	}

	public int getHoldability() throws SQLException {
		return this.realConnection.getHoldability();
	}

	public DatabaseMetaData getMetaData() throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

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

	public Map<String, Class<?>> getTypeMap() throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public SQLWarning getWarnings() throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public boolean isClosed() throws SQLException {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean isReadOnly() throws SQLException {
		// TODO Auto-generated method stub
		return false;
	}

	public String nativeSQL(String sql) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public CallableStatement prepareCall(String sql) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public CallableStatement prepareCall(String sql, int resultSetType,
			int resultSetConcurrency) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public CallableStatement prepareCall(String sql, int resultSetType,
			int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
			throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql, int[] columnIndexes)
			throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql, String[] columnNames)
			throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql, int resultSetType,
			int resultSetConcurrency) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public PreparedStatement prepareStatement(String sql, int resultSetType,
			int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public void releaseSavepoint(Savepoint savepoint) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void rollback() throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void rollback(Savepoint savepoint) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void setAutoCommit(boolean autoCommit) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void setCatalog(String catalog) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void setHoldability(int holdability) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void setReadOnly(boolean readOnly) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public Savepoint setSavepoint() throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public Savepoint setSavepoint(String name) throws SQLException {
		// TODO Auto-generated method stub
		return null;
	}

	public void setTransactionIsolation(int level) throws SQLException {
		// TODO Auto-generated method stub
		
	}

	public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
		// TODO Auto-generated method stub
		
	}

}


首先看到了这个类中有非常多恶心的代码,那些方法都是Connection接口中的,我们这里仅仅须要实现close方法就能够了。其它的方法中能够加入:

return this.realConnection.XXX

我们看到会在类中保留一个Connection对象,这个对象就是真实的连接对象,即我们使用

DriverManager.getConnection(url)

这样的方法获取到的。由于我们要在close方法中使用到这个真实的连接

我们看一下close方法吧:

public void close() throws SQLException {
		this.currentUseCount++;
		if(this.currentUseCount < this.maxUseCount){
			this.dataSource.free(this);
		}else{
			dataSource.currentCount--;
			this.realConnection.close();
		}
	}

首先看到我们定义了一个类变量:currentUseCount用来表示当前连接的使用次数。同一时候另一个类变量就是maxUseCount表示当前连接的最大使用次数。我们看一下close方法的逻辑:

首先当用户调用close方法的时候当前连接的使用数就加一,这里有些同学可能不能理解,我们想想上面还记得我们释放连接的时候是怎么做的,是将这个连接又一次放到池子中,所以这个连接又被用了一次。所以这里面是加一,当这个连接的当前使用次数没有超过他的最大使用次数的话,就还把他放到池子中(就是数据源中的free方法,这种方法中传递的參数是我们自己定义的连接对象,由于我们不是真的须要关闭这个连接的),假设使用次数超过了最大使用次数的话,我们就将这个连接真正的释放关闭了,同一时候须要将数据源中当前的连接数减去一,这里我们是调用真实连接的关闭方法的。所以我们须要在我们自己定义的连接中保持一个真实连接的对象,事实上我们採用的是组合的方法,在一个要想调用另外类中的方法。我们须要在本类中维持一个他的对象,然后进行调用他特定的方法。这样的方式也是一种设计模式叫做:静态代理,相当于我们本类是另外一个类的代理。

同一时候我们须要在构造函数中传递一个数据源对象进来的,当然我们这时候须要在之前的数据源中改动一下,这里改动非常easy的,仅仅须要改动数据源中的createConnection方法就能够了:

private Connection createConnection() throws SQLException{
		Connection realConn = DriverManager.getConnection(url);
		MyConnection myConnection = new MyConnection(realConn,this);
		return myConnection;
	}

我们返回的事实上是我们自己的定义的连接。这个连接事实上也是真实连接的一个代理对象。这样我们在JdbcUtils中的free方法中直接调用:

conn.close();

而不须要调用:

dataSource.free(conn);

这种释放方式就和我们之前普通连接的释放方式是一样的。

事实上我们上面做的这么多的操作就是为了这个,想让用户可以还是直接调用conn.close方法就行释放连接,我们还是执行一下之前的測试类:

package com.weijia.datasource;

import java.sql.Connection;

import com.weijia.firstdemo.JdbcUtils;

public class Test {
	
	public static void main(String[] args) throws Exception{
		for(int i=0;i<10;i++){
			Connection conn = JdbcUtils.getConnection();
			System.out.println(conn);
			JdbcUtils.free(null, null, conn);
		}
	}

}

执行结果例如以下:


我们看到前五个用的是同一个连接对象,这个原因就是我们在我们自己定义的连接MyConnection类中使用了当前连接的最大使用次数是5次

我们看到在定义我们自己的连接类的时候,须要实现Connection接口,这个接口中须要实现的方法非常多,事实上我们仅仅须要一个close方法就能够了,这时候我们还能够将我们的代码在改动一下,以下是我们改动之后的自己定义连接类:

package com.weijia.datasource;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;

public class MyConnectionHandler implements InvocationHandler{
	
	private Connection realConnection = null;
	private Connection warpedConnection = null;
	private MyDataSource2 dataSource = null;
	
	//当前连接的使用的次数
	private int maxUseCount = 5;
	private int currentUseCount = 0;
	
	public MyConnectionHandler(MyDataSource2 dataSource){
		this.dataSource = dataSource;
	}
	
	public Connection bind(Connection conn){
		this.realConnection = conn;
		warpedConnection = (Connection)Proxy.newProxyInstance(this.getClass().getClassLoader(),new Class[]{Connection.class},this);
		return warpedConnection;
	}

	public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
		if("close".equals(method.getName())){
			this.currentUseCount++;
			if(this.currentUseCount < this.maxUseCount){
				this.dataSource.free(warpedConnection);
			}else{
				dataSource.currentCount--;
				this.realConnection.close();
			}
		}
		return method.invoke(this.realConnection, args);
	}

}

这里我们看到了并没有实现Connection接口了。并且代码也是非常简洁的,事实上这个就是动态代理的模式,我们通过bind方法传递进来一个真实的连接对象。然后使用Proxy类实例化一个代理对象。newProxyInstance方法的參数说明:

第一个:须要代理对象的类载入器

第二个:须要代理对象实现的接口

第三个:InvocationHandler回调接口,我们基本的工具都是实现这个接口中的invoke方法

然后我们在invoke方法中拦截close方法就可以。将之前的close方法中的逻辑搬到这里就能够了。

我们使用以上測试代码执行例如以下:


这里我们就看到了使用动态代理非常easy的。可是有一个限制。就是代理对象必需要实现一个接口。这里正好是Connection接口,他比静态代理优雅了非常多的,后面我们在说到Spring的时候还会讲到这个动态代理模式的


好了,上面我们就能够看到我们自定义了一个数据源,连接,这样对我们后面的操作优化了非常多。


以下我们在来看一下apache的数据源DataSource,事实上这个数据源大体上和我们上面设计的以一样的,仅仅是他做了更优化。更好。

首先我们导入须要的jar包:


然后我们定义一个dbcpconfig.properties文件。用于配置数据源的相关信息:

#连接设置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=123456

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

#最大连接数量
maxActive=50

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

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

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


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

#指定由连接池所创建的连接的自己主动提交(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

从这些配置上我们能够看到前面的几个參数的含义就是我们自定义数据源中的使用到的,这里另一个參数是maxWait是超时。这个就是我们在获取连接的时候,当连接数超过最大连接数的时候,须要等待的时间,在前面我们自定义的数据源中我们是採用抛出异常的问题来解决的,这里我们看到apache是採用线程等待的方式来解决的。
我们在代码里面改动的东西也是非常少的,在JdbcUtils中的静态代码块中使用apache的数据源就可以:
//使用Apache的DBCP数据源
Properties prop = new Properties();
prop.load(JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties"));
dataSource = BasicDataSourceFactory.createDataSource(prop);

apache中的连接也是经过改装的,我们直接调用conn.close方法就可以。和我们上面实现的是一样的。


JDBC中CRUD的模板模式

我们从前面的样例中可以看到,我们在操作CRUD的时候,返现有非常多反复的代码,比方如今一个UserDao来操作查询操作。写了一段查询代码,然后有一个ProductDao也来操作查询操作,也写了一段查询代码,事实上我们会发现这两个查询代码中有非常多是反复的,这时候我们就想了。能不可以进行代码的优化,我们想到了模板模式,就是将同样的代码提取出来放到父类中做,不同的代码放到各自的子类中去做,这样反复的代码仅仅会出现一次了,以下来看一下实例。首先我们看一下抽象出来的Dao代码:

package com.weijia.template;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.weijia.domain.DaoException;
import com.weijia.firstdemo.JdbcUtils;

public abstract class AbstractDao {
	
	/**
	 * 更新
	 */
	protected int update(String sql,Object[] args) {
		//这里须要做推断的,可能args为null
		Connection conn = null;
		PreparedStatement st = null;
		try{
			conn = JdbcUtils.getConnection();
			st = conn.prepareStatement(sql);
			for(int i=0;i<args.length;i++){
				st.setObject(i+1, args[i]);
			}
			int count = 0;
			count = st.executeUpdate();
			System.out.println("更新的记录数:"+count);
			return count;
		}catch(Exception e){
			throw new DaoException(e.getMessage(),e);
		}finally{
			JdbcUtils.free(null, st, conn);
		}
	}
	
	/**
	 * 查询
	 * @param sql
	 * @param args
	 * @return
	 */
	protected Object find(String sql,Object[] args){
		Connection conn = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			st = conn.prepareStatement(sql);
			for(int i=0;i<args.length;i++){
				st.setObject(i+1, args[i]);
			}
			rs = st.executeQuery();
			Object obj = null;
			while(rs.next()){
				//不同的部分放到子类去做
				obj = rowMapper(rs);
			}
			return obj;
		}catch(Exception e){
			throw new DaoException(e.getMessage(),e);
		}finally{
			JdbcUtils.free(null, st, conn);
		}
	}
	
	//子类须要实现的结果集处理方法
	protected abstract Object rowMapper(ResultSet rs) throws SQLException;

}


看一下UserDaoImpl类:

package com.weijia.template;

import java.sql.ResultSet;
import java.sql.SQLException;

import com.weijia.domain.User;

public class UserDaoImpl extends AbstractDao{

	/**
	 * 更新用户信息
	 */
	public int update(User user) {
		String sql = "udpate user set name=?,birthday=?,money=?,where id=?

"; Object[] args = new Object[]{user.getName(),user.getBirthday(),user.getMoney(),user.getId()}; return super.update(sql, args);//同样的代码调用父类的方法就可以 } /** * 删除用户 * @param user */ public void delete(User user){ String sql = "delete from user where id=?"; Object[] args = new Object[]{user.getId()}; super.update(sql, args); } /** * 查找用户 * @param loginName * @param password * @return */ public User findUser(String loginName){ String sql = "select id,name,money,birthday from user where name=?"; Object[] args = new Object[]{loginName}; return (User)super.find(sql, args); } @Override protected Object rowMapper(ResultSet rs) throws SQLException{ User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setMoney(rs.getFloat("money")); user.setBirthday(rs.getDate("birthday")); return user; } //假设insert的时候不须要获取主键的话。也能够使用super.update方法实现的。这样代码就显得非常整洁。同样的代码仅仅须要一份就可以(放在父类中) //不同的地方放到子类来实现 //首先要区分哪些是变动的部分。哪些是不变的部分就可以 }


ProductDaoImpl类:

package com.weijia.template;

import java.sql.ResultSet;

public class ProductDaoImpl extends AbstractDao{
	
	public int update(){
		String sql = "update product set pname=?,price=? where pid=?";
		Object[] args = new Object[]{"drug",11,1};
		return super.update(sql, args);
	}

	@Override
	protected Object rowMapper(ResultSet rs) {
		return null;
	}

}

看到了,这样来实现的话,代码就非常简洁了,这里的ProductDaoImpl类中没有写完,就是大概是那个意思。这里体现出了一个设计模式就是:模板模式

接着看,如今有一个问题,就是查询,事实上update的方式非常easy的,全然能够统一化的。由于查询须要处理查询之后的结果集,所以非常纠结的,上面的样例中我们看到,我们查询的是一个User对象,假如如今我仅仅是想查询一个用户的name,那么我们仅仅能在写一个findUserName方法了,同一时候还须要在AbstractDao父类中加入一个抽象方法的行映射器,这样的方式就非常纠结了。假如我们还有其它的查询须要的话。反复的代码又開始多了,这里我们将採用策略模式进行解决,我们仅仅须要定义行映射器的接口:

package com.weijia.strategy;

import java.sql.ResultSet;
import java.sql.SQLException;

public interface RowMapper {

	public Object mapRow(ResultSet rs) throws SQLException;
	
}


在父类中仅仅须要改动一下查询的方法:

/**
	 * 查找用户
	 * @param sql
	 * @param args
	 * @param rowMapper
	 * @return
	 */
	protected Object find(String sql,Object[] args,RowMapper rowMapper){
		Connection conn = null;
		PreparedStatement st = null;
		ResultSet rs = null;
		try{
			conn = JdbcUtils.getConnection();
			st = conn.prepareStatement(sql);
			for(int i=0;i<args.length;i++){
				st.setObject(i+1, args[i]);
			}
			rs = st.executeQuery();
			Object obj = null;
			while(rs.next()){
				obj = rowMapper.mapRow(rs);
			}
			return obj;
		}catch(Exception e){
			throw new DaoException(e.getMessage(),e);
		}finally{
			JdbcUtils.free(null, st, conn);
		}
	}

加入了一个RowMapper接口变量


然后在子类中实现这个接口就可以:

/**
	 * 查询名称
	 * @param id
	 * @return
	 */
	public String findUserName(int id){
		String sql = "select name from user where id=?";
		Object[] args = new Object[]{id};
		Object user = super.find(sql, args,new RowMapper(){
			public Object mapRow(ResultSet rs) throws SQLException {
				return rs.getObject("name");
			}
		});
		return ((User)user).getName();
	}
	
	/**
	 * 採用策略模式:传递不同的行为:C++中能够使用函数指针来实现,Java中能够使用接口的回调来实现
	 * @param loginName
	 * @param password
	 * @return
	 */
	public User findUser(String loginName){
		String sql = "select id,name,money,birthday from user where name=?";
		Object[] args = new Object[]{loginName};
		return (User)super.find(sql, args,new RowMapper(){

			public Object mapRow(ResultSet rs) throws SQLException {
				User user = new User();
				user.setId(rs.getInt("id"));
				user.setName(rs.getString("name"));
				user.setMoney(rs.getFloat("money"));
				user.setBirthday(rs.getDate("birthday"));
				return user;
			}
			
		});
	}


我们能够看到这两个查询的方法就非常优雅了。这样做了之后。我们仅仅须要在指定的子类中加入指定的方法就可以,事实上策略模式非常easy的,就是相当于回调机制,就是想运行指定的方法,可是Java中没有函数指针。C++中事实上能够的,所以仅仅能通过回调来实现了。

通过上面的CRUD优化之后,我们在进行操作的时候,代码编写是非常方便和简洁的


Spring框架中的JdbcTemplate

说完了上面的我们自己定义的CRUD模板,以下我来看一下Spring框架给我们提供的CRUD模板(JdbcTemplate),事实上他的实现原理和我们上面是一样的,仅仅是他的功能会更强。


以下来看一下实例代码:

package com.weijia.springtemplate;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;

import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import com.weijia.domain.User;
import com.weijia.firstdemo.JdbcUtils;

public class JdbcTemplateTest {
	
	public static void main(String[] args){
		User user = new User();
		user.setMoney(20);
		user.setId(1);
		update(user);
	}
	
	/**
	 * 更新操作
	 * @param user
	 */
	static void update(User user){
		JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource());
		String sql = "update user set money=? where id=?

"; Object[] args = new Object[]{user.getMoney(),user.getId()}; jdbc.update(sql, args); } /** * 通过username查询用户 * @param name * @return */ static User findUser(String name){ //须要传递一个数据源 JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where name=?"; Object[] args = new Object[]{name}; //queryForObject方法和我们之前採用策略模式设置的模板非常相似呀,这种方法仅仅会返回一个记录。假设有多个记录返回或者没有记录返回的话。这种方法就会报告异常的 Object user = jdbc.queryForObject(sql,args,new RowMapper(){ public Object mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setMoney(rs.getFloat("money")); user.setBirthday(rs.getDate("birthday")); return user; }}); return (User)user; } /** * 通过username查询实体类 * @param name * @return */ static User findUsers(String name){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where name=?

"; Object[] args = new Object[]{name}; //假设没有记录或者返回多个记录的话,这种方法是会报异常的 //使用这种方法直接将返回的结果集映射到实体类。这里返回的结果集中的字段和实体类中的属性名必须相等 //假设不相等的话,就是用默认值对其属性进行赋值 Object user = jdbc.queryForObject(sql,args,new BeanPropertyRowMapper(User.class){}); return (User)user; } /** * 查询多个用户 * @param id * @return */ static List findUser1(int id){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where id<?"; Object[] args = new Object[]{id}; List users = jdbc.query(sql,args,new BeanPropertyRowMapper(User.class){}); return users; } //求最大值,记录总数等情况,查询结果仅仅有一个值 //返回8种基本类型 static int getUserCount(){ String sql = "select count(*) from user"; //JdbcTemplate是线程安全的 JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); return jdbc.queryForInt(sql); } //返回String static String getUserName(int id){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select name from user where id="+id; Object name = jdbc.queryForObject(sql, String.class); return (String)name; } //返回map static Map getUser(int id){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,birthday from user where id=?

"; return jdbc.queryForMap(sql,new Object[]{id}); } //加入完用户之后返回主键 static User addUser(final User user){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); //这个和RowMapper接口差点儿相同。RowMapper是传回来一个ResultSet //而这个接口返回的是一个Connection。给我们很多其它的权限了 jdbc.execute(new ConnectionCallback(){ public Object doInConnection(Connection conn) throws SQLException,DataAccessException { String sql = "insert into user(name,birtdhday,birthday) values('jiangwei','1987-01-01',400)"; PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.executeUpdate(); //可能是组合主键,可能会返回一个ResultSet ResultSet rs = ps.getGeneratedKeys(); if(rs.next()){ user.setId(rs.getInt(1)); } return null; }}); return user; } }


以下来看一下,JdbcTemplate的相关用法:

首先看一下,我们開始使用一个数据源来初始化一个JdbcTemplate模板

JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource());

然后仅仅须要直接运行其相关的方法就可以:

update(String sql,Object[] args):第一个參数是sql语句,第二參数是须要填充的更新參数
queryForObject(String sql,Object[] args, RowMapper rowMapper):第一參数是sql语句。第二參数是须要填充的查询參数,第三个參数是行映射器(和前面我们设计的一样),这种方法仅仅适用查询结果是一个的情况,假设查询结果是多个的话,这种方法会报异常的,同一时候这种方法第三个參数我们也能够传递一个:new BeanPropertyRowMapper(User.class){}对象,这个就能够将查询结果填充到User实体类中了,当然这里有一个限制就是要求查询出来的结果集中的字段名和实体类中的属性名一样。事实上这内部使用的是反射技术来实现的,我们之前写过这种方法的。

query(String sql,Object[]args,RowMapper rowMapper):这种方法和上面的那个方法不同的就是返回的结果。这种方法返回的是一个List。针对于查询结果是多个的情况

queryForInt(String sql,Object[] args):这种方法是针对查询结果是一个整型的。比方我们须要查询出用户的总数

queryForLong(String sql,Object[] args):这种方法是查询出long类型的

queryForObject(String sql, Class requiredType):这种方法是对于那些没有特定查询类型的方法同一使用这种方法。比方如今想查询一个用户的名称是String类型的,或者想查询用户的money,是float类型的。这里我们仅仅须要在第二个參数中指定类型就可以

queryForMap(String sql,Object[] args):查询返回的是一个Map集合

queryForList(String sql,Object[] args):查询返回的是一个List集合

上面的方法我们就足够了


以下再来看一个需求,假设我们想得到插入一条记录之后的主键的操作。这里改怎样操作呢?

在之前我们操作的是须要将PreparedStatement中设置一个參数,不然会报错的,我们通过上面的方法可以知道其内部都是使用PreparedStatement实现的。由于有占位符。须要设置查询參数的。

可是他并没有提供一个方法可以设置这个PreparedStatement的一些參数。可是假设我们想获取到主键值的话,必须要设置PreparedStatement的第二參数为:Statement.RETURN_GENERATED_KEYS,那么这时候。JdbcTemplate还给我们提供了一个方法:

jdbc.execute(new ConnectionCallback(){
	public Object doInConnection(Connection con) throws SQLException,DataAccessException {
		//do something
		return null;
	}
});

execute方法中传递一个Connection的回调类,然后实现一个方法,在方法中我们能够获取到当前的连接,当我们拿到了这个连接的话。就能够进行操作了。

这样一来,上面提到的JdbcTemplate中的方法就能够满足我们的日常需求了


加强版的JdbcTemplate

1.NamedParameterJdbcTemplate

这个模板事实上我们见名知意。他的最大的作用应该是參数名称的功能,我们在上面的模板中能够看到。我们每次传递的參数数组中的參数顺序必须和查询语句中的占位符的顺序要同样,可是这个模板给我们提供了一个便捷的优点就是。不须要关心參数的顺序:

static User findUsers(User user){
		String sql = "select id,name,money,birthday from user where name=:n and money>:m and id<:id";
		//參数的顺序必须一致,不然报错
		//Object[] args = new Object[]{user.getName(),user.getMoney(),user.getId()};
		//假设没有记录或者返回多个记录的话。这种方法是会报异常的
		//使用Map取代数据进行传递參数。这样就不须要在乎传递的顺序了(事实上内部源码是先解析这个map,将从新组装sql然后交给JdbcTemplate来处理)
		Map<String,Object> params = new HashMap<String,Object>();
		params.put("n", user.getName());
		params.put("m", user.getMoney());
		params.put("id", user.getId());
		Object users = named.queryForObject(sql, params, new BeanPropertyRowMapper(User.class));
		return (User)users;
	}
这里要注意的是查询语句sql中不在使用占位符了。而是使用參数变量的形式:

:參数名

这个參数名,我们会在以下的HashMap中当做key来进行參数的填充。然后将这个HashMap变量传递到queryForObject方法中,而不是在使用数组的方式了。这样的方式事实上就是将数组类型的參数传递变成了Map类型的參数传递,事实上方法的功能都是没有改变的,仅仅是相相应的方法中的參数不再是Object[]数组了,而是Map集合类型的


相同的假如是JavaBean类型的怎么填充參数呢?这里也提供了一个方法:

SqlParameterSource ps = new BeanPropertySqlParameterSource(user);

这样直接进行參数的填充,可是这里有一个限制就是sql中的參数名称必需要和JavaBean中的属性名称一样(内部使用反射技术实现的)


最后我们获取主键的值的方式也变化了:

SqlParameterSource ps = new BeanPropertySqlParameterSource(user);
//将插入之后的记录的id放到keyHolder中
KeyHolder keyHolder = new GeneratedKeyHolder();
named.update(sql, ps, keyHolder);
//也有可能是组合主键
Map map = keyHolder.getKeys();
int id = keyHolder.getKey().intValue();

这样比JdbcTemplate中的方法更简单了。


以下看一下完整的实例代码:

package com.weijia.springtemplate;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import com.weijia.domain.User;
import com.weijia.firstdemo.JdbcUtils;

/**
 * 能够实现命名參数
 * 这样就不会操心占位符?和传递參数的位置了错乱的问题了
 * 将数组參数变成hashmap类型的參数
 * @author weijiang204321
 *
 */
public class NamedJdbcTemplate {
	
	public static void main(String[] args) throws Exception{
		System.out.println("add id:"+addUser(new User(1,"jiangwei",new Date(System.currentTimeMillis()),100)));
	}
	
	//其内部有一个JdbcTemplate对象,有些事情还是交给JdbcTemplate来处理的(静态代理)
	public static NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(JdbcUtils.getDataSource());
	
	static User findUser(User user){
		JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource());
		String sql = "select id,name,money,birthday from user where name=? and money>? and id<?";
		//參数的顺序必须一致,不然报错
		Object[] args = new Object[]{user.getName(),user.getMoney(),user.getId()};
		//假设没有记录或者返回多个记录的话,这种方法是会报异常的
		Object users = jdbc.queryForObject(sql,args,new BeanPropertyRowMapper(User.class){});
		return (User)users;
	}
	
	static User findUsers(User user){
		String sql = "select id,name,money,birthday from user where name=:n and money>:m and id<:id";
		//參数的顺序必须一致,不然报错
		//Object[] args = new Object[]{user.getName(),user.getMoney(),user.getId()};
		//假设没有记录或者返回多个记录的话,这种方法是会报异常的
		
		//使用Map取代数据进行传递參数。这样就不须要在乎传递的顺序了(事实上内部源码是先解析这个map,将从新组装sql然后交给JdbcTemplate来处理)
		Map<String,Object> params = new HashMap<String,Object>();
		params.put("n", user.getName());
		params.put("m", user.getMoney());
		params.put("id", user.getId());
		Object users = named.queryForObject(sql, params, new BeanPropertyRowMapper(User.class));
		return (User)users;
	}
	
	static User findUser1(User user){
		String sql = "select id,name,money,birthday from user where name=:name and money>:money and id<:id";
		//參数名必须和User中的属性名同样的。内部还是通过反射技术实现的
		SqlParameterSource ps = new BeanPropertySqlParameterSource(user);
		Object users = named.queryForObject(sql, ps, new BeanPropertyRowMapper(User.class));
		return (User)users;
	}
	
	static int addUser(User user){
		String sql = "insert into user(name,birthday,money) values(:name,:birthday,:money)";
		//參数名必须和User中的属性名同样的,内部还是通过反射技术实现的
		SqlParameterSource ps = new BeanPropertySqlParameterSource(user);
		//将插入之后的记录的id放到keyHolder中
		KeyHolder keyHolder = new GeneratedKeyHolder();
		named.update(sql, ps, keyHolder);
		//也有可能是组合主键
		Map map = keyHolder.getKeys();
		int id = keyHolder.getKey().intValue();
		return keyHolder.getKey().intValue();
	}

}


2.SimpleJdbcTemplate

见名知意。这个模板会操作变得更简单的,他的主要变化就是两点:

第一、实现可变參数了
第二、查询出来的对象无须进行类型转换了

//将类型也作为參数传入的,无须进行类型转换操作了
static User findUser(int id,String name,Class<User> clazz){
	String sql = "select id,name,money,birthday from user where id=?

and name=?

"; //參数名必须和User中的属性名同样的,内部还是通过反射技术实现的 //simple.getNamedParameterJdbcOperations();//得到一个NamedParameterJdbcOperateions对象 //simple.getJdbcOperations();//得到Jdbc对象 //这种方法在传递參数的时候使用的是可变參数,參数的顺序和占位符?要必须一致 return simple.queryForObject(sql, ParameterizedBeanPropertyRowMapper.newInstance(clazz),id,name); }


我们看到queryForObject方法中第二个參数是ParameterizedBeanPropertyRowMapper.newInstance(Class clazz),传递进去的是查询出来的实体类的类型class,这样我们在返回的时候,就不须要进行类型转化了

最后一个參数事实上是可变參数。能够传递多个值的。可是这个传递的值的顺序必需要和上面sql中占位符的顺序一致,以下是測试代码:

System.out.println(findUser(1505,"jiangwei",User.class));


总结:

至此我们就将JDBC的相关知识都介绍完成了,由于JDBC本身的内容是非常多的,我们也仅仅有遇到问题的时候採取解决。上面的仅仅是对于开发来说应该没有多大的问题了,可能会有一些细节上的问题,这个仅仅能在兴许进行完好了。

上面说到的内容。可能有些不全。我将整个解说的项目功能放到了网上。下载地址:

http://download.csdn.net/detail/jiangwei0910410003/7373133

这个须要注意的是一定记得连接上正确的数据库。这个能够查看连接数据库的那段代码。。

假设发现有问题,请及时提醒。我做改动。希望可以和大家一起做到百分百的成功。。










版权声明:本文博主原创文章,博客,未经同意不得转载。

posted @ 2015-09-23 19:03  hrhguanli  阅读(314)  评论(0编辑  收藏  举报