这是我的JDBC第三次学习了,在学习的过程中,老是会忘掉一些知识,不记下笔记实在不行啊!
使用JDBC调用存储过程
(1)关于如何使用Navicat(11.1.13) for MySQL如何创建存储过程。我在另一篇文章写过,在此不赘述了。
使用Navicat(11.1.13) for MySQL如何创建存储过程,存储过程的主要代码如下:
BEGININSERT INTO user (name, birthday, money) values (pname, birthday, money);SELECT LAST_INSERT_ID() into pid; END
注意:
- in:表示输入参数。
- out:表示输出参数。
- last_insert_id()是mysql所特有的一个函数,可以查询出最后一次插入到数据库的那条数据的id。
(2)使用JDBC调用存储过程(即返回当前这条记录插入后形成的id),代码如下:
static void ps() throws SQLException {Connection conn = null;CallableStatement cs = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();/** call:是固定写法,addUser()是我们在数据库中定义的存储过程的名字* ()后面指定参数 ,如果没有任何参数addUser后的括号也要写上。* 所以,类似于函数 */String sql = "{call addUser(?,?,?,?)}";cs = conn.prepareCall(sql);cs.registerOutParameter(4, Types.INTEGER);//注册输出参数/** 设置输入参数*/cs.setString(1, "pa name");cs.setDate(2, new java.sql.Date(System.currentTimeMillis()));cs.setFloat(3, 100f);cs.executeUpdate();int id = cs.getInt(4);//不注册输出参数是不能这样拿出来的System.out.println("id="+id);} finally {JdbcUtils.free(rs, cs, conn);}}
返回当前这条记录插入后形成的id,还有如下你方法:
static int create() throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立连接conn = JdbcUtils.getConnection();//3.创建语句String sql = "insert into user (name,birthday,money) values ('name1 gk','1987-01-01',400)";/** 自动产生主键,用参数Statement.RETURN_GENERATED_KEYS拿出产生的主键* mysql参数Statement.RETURN_GENERATED_KEYS加不加都可以拿出来* 但是这和不同的数据库产品以及相应的驱动有关,所以最好写上!!!*/ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);ps.executeUpdate();/** getGeneratedKeys()这个方法为什么不是返回int类型而是要返回一个ResultSet类型呢?* 因为,一、主键不一定是int类型,* 二、联合(复合)主键 ,有可能是一张表中的几个字段合起来构成一个id,这样就不能返回一个int类型了,* 如果是联合主键返回的是多列的内容,我们可以遍历ResultSet得到联合主键列的值。 * 所以返回ResultSet*/rs = ps.getGeneratedKeys();int id = 0;if(rs.next()) {id = rs.getInt(1);}return id;} finally {JdbcUtils.free(rs, ps, conn);}}
使用JDBC的批处理功能
批处理,可以大幅度提升大量增、删、改的速度。
public class BatchTest {//main方法调用测试批量插入与普通的insert所消耗的时间比 public static void main(String[] args) throws SQLException {long start = System.currentTimeMillis();for(int i = 0; i < 100; i++) {create(i);}long end = System.currentTimeMillis();System.out.println("create:"+(end-start));start = System.currentTimeMillis();createBatch();end = System.currentTimeMillis();System.out.println("createBatch:"+(end-start));}//普通方法插入数据static void create(int i) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立连接conn = JdbcUtils.getConnection();//3.创建语句String sql = "insert into user (name,birthday,money) values (?,?,?)";ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);ps.setString(1, "batch name"+i);ps.setDate(2, new java.sql.Date(System.currentTimeMillis()));ps.setFloat(3, 100f+i);ps.executeUpdate();} finally {JdbcUtils.free(rs, ps, conn);}}//批量插入数据static void createBatch() throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {//2.建立连接conn = JdbcUtils.getConnection();//3.创建语句String sql = "insert into user (name,birthday,money) values (?,?,?)";ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);/** 每循环一次都会形成一条新的sql语句被打包,直到循环完成然后进行批量的处理* 那么可不可以无限量的增加呢?其实会产生内存溢出的情况,到底循环多少次进行打包才合适,这个值要经过测试*/for(int i = 0; i < 100; i++) {ps.setString(1, "batch name"+i);ps.setDate(2, new java.sql.Date(System.currentTimeMillis()));ps.setFloat(3, 100f+i);/** 并不是使用批处理就会提高效率* 把sql语句打成一个包* 包不能太大(并不是越大越好),会内存溢出*/ps.addBatch();}int[] is = ps.executeBatch();} finally {JdbcUtils.free(rs, ps, conn);}}}
可滚动结果集与分页技术
static void scroll() throws SQLException {Connection conn = null;Statement st = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();/** 在创建一个Statement的时候指定可滚动的结果集的类型* TYPE_SCROLL_SENSITIVE:滚动的过程中,对数据库是敏感的* (按我的理解就是查询数据的时候,如果又新增、删除、更新,那么能感觉得到)* CONCUR_READ_ONLY:字面意思是同意只读*/st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);/** 在mysql中通过limit关键字实现分页,* 每种数据库产品的关键字不同例如:Oracle使用rownum,sqlServer用top* 现在有几十种关系型数据库,如果数据库不支持这种关键字进行分页的时候可以用滚动的结果集来实现分页,* 但是性能比较低 *///sql = "select id,name,birthday,money from user limit 150, 10";//mysql支持分页rs = st.executeQuery("select id,name,birthday,money from user");//5.处理结果while(rs.next()) {System.out.println(rs.getObject("id")+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));}System.out.println("-------------------------------------");/** 绝对定位,可以直接定位到rs所有返回结果中指定的一条记录上 * 例定位到第150行*/rs.absolute(150);int i = 0;/** 可以通过i来控制循环次数,实现分页效果* 但是要数据库产品或者驱动支持此功能! */while(rs.next() && i < 10) {i++;System.out.println(rs.getObject("id")+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));}} finally {JdbcUtils.free(rs, st, conn);}}
可更新和对更新敏感的结果集
static void read() throws SQLException, InterruptedException {Connection conn = null;Statement st = null;ResultSet rs = null;try {//2.建立连接conn = JdbcUtils.getConnection();//3.创建语句/** 设置滚动结果集的类型为:ResultSet.TYPE_SCROLL_SENSITIVE,就是能感知到数据库的变化 * CONCUR_UPDATABLE:字面意思是同意更新*/st = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);//4.执行语句rs = st.executeQuery("select id,name,birthday,money from user where id < 5");//5.处理结果/** 验证TYPE_SCROLL_SENSITIVE* 下面让rs每循环一次rs睡眠10秒钟,然后再这个过程中我们用mysql客户端修改数据库中的数据 * 我们看看它读出来的是修改前的数据,还是修改后的。我们在上面设置的可滚动的结果集的类型 * 是ResultSet.TYPE_SCROLL_SENSITIVE,也就是能感知数据库的变化,那如果在rs没有读出数据库里的* 那条数据之前我们在mysql的客户端将原先的数据修改掉,这里读出来的数据应该是修改后的数据,但是在* 测试的时候读出的数据却依然是修改之前的,这应该和数据库的驱动有关系 * 但是如果是能感知数据库的变化,那么数据库的性能也是降低了,你执行executeQuery()方法后,它已经将数据查询完成* 打包后给你发送过来了,如果察觉到数据库的变化那么它要在输出之前再查询一遍数据库,这种需求用的比较少,作为了解即可 */while(rs.next()) {int id = rs.getInt("id");System.out.println("show " + id + "...");Thread.sleep(10000);System.out.println(id+"\t"+rs.getObject("name")+"\t"+rs.getObject("birthday")+"\t"+rs.getObject("money"));/** 查询的时候可以更新(可更新的结果集)* 可更新的结果集,我们并不建议这样做因为上面的sql语句是查询操作 * 但是下面还隐藏着更新操作,对于程序的可读性不好,这种需求也比较少 * 了解即可*/String name = rs.getString("name");if("lisi".equals(name)) {rs.updateFloat("money", 300f);rs.updateRow();//修改完成后要修改一行 }}} finally {JdbcUtils.free(rs, st, conn);}}
数据库的元数据信息
通过DatabaseMetaData可以获得数据库相关的信息如:数据库版本、数据库名、数据库厂商信息、是否支持事务、是否支持某种事务隔离级别,是否支持滚动结果集等。对于我们编写程序来说不常用,但是在框架的编写中经常会用到,例如hibernate,它要屏蔽不同数据库之间的区别,那么它就要知道当前是什么数据库,然后做出相应的判断处理:在使用hibernate的时候有一项是配置数据库的方言,其实就是指定你使用的是什么数据库产品。如果你不进行指定,hibernate会自动的尝试着去检测当前数据库产品的类型,其实就是根据DatabaseMetaData来检测的。
示例代码:
public class DBMO {public static void main(String[] args) throws SQLException {Connection conn = JdbcUtils.getConnection();//得到数据库的元信息 DatabaseMetaData dbmd = conn.getMetaData();//取出当前使用数据库的名称 System.out.println("db name:" + dbmd.getDatabaseProductName());//看看当前数据库支不支持事务 System.out.println("tx:" + dbmd.supportsTransactions());conn.close();}}
参数的元数据信息
public class ParameterMetaTest {public static void main(String[] args) throws SQLException {Object[] params = new Object[] {"lisi", new java.sql.Date(System.currentTimeMillis()), 100f};read("select * from user where name = ? and birthday < ? and money > ?", params);}static void read(String sql, Object[] params) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();ps = conn.prepareStatement(sql);//得到参数信息的元数据 /*第一种方式给sql语句中的占位符赋值,但是要约定:sql语句中占位符的所表示的类型和个数和参数数组中是一致的 ParameterMetaData pmd = ps.getParameterMetaData();int count = pmd.getParameterCount();for(int i = 1; i <= count; i++) {System.out.print(pmd.getParameterClassName(i) + "\t");System.out.print(pmd.getParameterType(i) + "\t");System.out.println(pmd.getParameterTypeName(i));//Ctrl+T打开其基础体系ps.setObject(i, params[i-1]);}*//** 第二种方式给sql语句中的占位符赋值*/for(int i = 1; i <= params.length; i++) {ps.setObject(i, params[i-1]);}rs = ps.executeQuery();while(rs.next()) {System.out.println(rs.getInt("id")+"\t"+rs.getString("name")+"\t"+rs.getDate("birthday")+"\t"+rs.getFloat("money"));}} finally {JdbcUtils.free(rs, ps, conn);}} }
小知识:快捷键Ctrl+T——打开其整个继承体系。
有可能产生的异常:
java.sql.SQLException: Parameter metadata not available for the given statement
解决的方法不难,就是在连接数据库时的URL后面加上可以返回的元数据类型
例如出异常时,我的URL是这样写的:
url = "jdbc:mysql://localhost:3306/jdbc";
正确写法应该是:
url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";
出现异常的原因:因为mysql驱动默认generateSimpleParameterMetadata=false只有设置为true,metadata类型会将每一个参数反射为Varchar类型。(时间才过去几天,就有点不是很清楚了)
将结果集元数据封装为Map
现在我们有一种需求将ResultSet结果集中的数据封装成Map,map的key是数据库中字段的值,value就是在字段中的值。
通过ResultSetMetaData可以获得结果有几列、各列名、各列别名、各列类型等。
可以将ResultSet放入Map(key:列名 value:列值)。
用反射ResultSetMetaData将查询结果读入对象中(简单的O/RMapping)。
- 让SQL语句中列别名和要读入的对象属性名一样;
- 通过ResultSetMetaData获得结果列数和列别名;
- 通过反射将对象的所有setXxx方法找到;
- 将3中找到的方法setXxx和2中找到的列别名进行匹配(即方法中的Xxx与列别名相等);
- 由上一步找到的方法和列别名对应关系进行赋值
Method.invoke(obj, rs.getObject(columnAliasName));
示例代码如下:
public class ResultSetMetaDataTest {public static void main(String[] args) throws SQLException {List<Map<String, Object>> data = read("select id, name as n from user where id < 5");System.out.println(data);}static List<Map<String, Object>> read(String sql) throws SQLException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {conn = JdbcUtils.getConnection();ps = conn.prepareStatement(sql);rs = ps.executeQuery();//得到ResultSet的元数据 ResultSetMetaData rsmd = rs.getMetaData();//得到ResultSet元数据的列数 int count = rsmd.getColumnCount();String[] colNames = new String[count];for(int i = 1; i <= count; i++) {System.out.print(rsmd.getColumnClassName(i)+"\t");//得到参数的类名,例java.lang.String System.out.print(rsmd.getColumnName(i)+"\t");//取列的实际名字System.out.println(rsmd.getColumnLabel(i));//取列的别名colNames[i-1] = rsmd.getColumnLabel(i);}List<Map<String, Object>> datas = new ArrayList<Map<String, Object>>();while(rs.next()) {Map<String, Object> data = new HashMap<String, Object>();for(int i = 0; i < colNames.length; i++) {data.put(colNames[i], rs.getObject(colNames[i]));}datas.add(data);}return datas;} finally {JdbcUtils.free(rs, ps, conn);}} }
编写一个基本的连接池来实现连接的复用
大家都知道Arraylist的底层使用数组实现的,而LinkedList使用链表实现的,所以对于Arraylist读取速度比较快而对于LinkedList修改和添加比较快,所以我们这个连接池因为要频繁的操作集合所以用LinkedList来实现。
public class MyDataSource {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";/** 对集合中的元素进行频繁的取出,* 用LinkedList*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();//向我们的LinkedList集合中加入10个链接作为我们的连接池 public MyDataSource() {try {for(int i = 0; i < 10; i++) {//将Connection放到链表的最后面 this.connectionsPool.addLast(this.createConnection());}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}//得到一个链接(先进先出算法)public Connection getConnection() throws SQLException {return this.connectionsPool.removeFirst();}//创建链接private Connection createConnection() throws SQLException {return DriverManager.getConnection(url, user, password);}/** 关闭一个链接,这个关闭不是真正意义上的关闭,* 而是又把它放回到连接池中,实现了Connection的复用 */public void free(Connection conn) {this.connectionsPool.addLast(conn);} }
对基本连接池进行一些工程细节上的优化
在上面实现的连接池中我们只是默认创建了10个连接,但是如果这个时候有10个线程同时都来拿连接,那连接池里就没有连接了,在有线程过来拿的时候就会报错了,现在我们进行一些优化。
public class MyDataSource {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";//规定默认创建的连接数 private static int initCount = 5;//规定最大可以创建的连接数 private static int maxCount = 10;//统计当前共创建了多少个连接 private int currentCount = 0;/** 对集合中的元素进行频繁的取出,* 用LinkedList*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();public MyDataSource() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());//每创建一个链接,currentCount++ this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {/** 因为Connection不是线程安全的,* 所以我必须保证每个线程拿到的链接不是同一个,所以要进行同步:当两个线程同时来拿的时候 * 另外一个线程必须等待 */synchronized (connectionsPool) {//①连接池中还有连接,取出if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();//②连接池中已没有连接,并且当前创建的链接数没有到最大值,那就继续创建链接 if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}//③大于连接池中的最大数,抛出异常throw new SQLException("已没有连接");}}public void free(Connection conn) {this.connectionsPool.addLast(conn);}private Connection createConnection() throws SQLException {return DriverManager.getConnection(url, user, password);} }
通过代理模式来保持用户关闭连接的习惯
在上面的示例中我们在关闭链接的时候,调用的是free方法来把这个连接又放回到了池中,但是按照开发人员的使用习惯应该是调用colse()方法来关闭一个链接,但是如果调用close方法关闭,那这个连接就真的关闭了,也就是说我们这个方法设计的不符合开发人员的使用习惯。下面我用代理模式(关于代理模式,我在另一篇文章中写过)的方法来解决这个问题:
定义一个类实现Connection接口,Connectio接口中有很多的方法,这些方法我们都无法自己完成,我们交给通过构造方法传递进来的真正的Connection的对象来完成,我们只是修改它的close方法,在用户得到链接的时候我们返回给用户这个类的对象,那么当用户调用close方法关闭链接的时候,我们就可以在这个close方法中将用户要关闭的那个链接再次的放到连接池中,这样链接就不会真正的关闭了。
public class MyConnetion implements Connection {private Connection realConnection;private MyDataSource2 dataSource;/** 限制其(连接)最大使用次数*/private int maxUseCount = 5;/** 记录(连接)当前使用次数*/private int currentUseCount = 0;/** 由于访问修饰符是default,* 所以只能在包cn.itcast.jdbc.datasource中使用MyConnetion*/MyConnetion(Connection connection, MyDataSource2 dataSource) {this.realConnection = connection;this.dataSource = dataSource;}//清除警告 @Overridepublic void clearWarnings() throws SQLException {this.realConnection.clearWarnings();}@Overridepublic Statement createStatement() throws SQLException {return this.realConnection.createStatement();}@Overridepublic void commit() throws SQLException {this.realConnection.commit();}@Overridepublic void close() throws SQLException {this.currentUseCount++;/** 规定同一个链接只能使用maxUseCount次,* 超过这个次数就把真正的链接关闭,连接池中就要少一个链接* 这个时候再拿链接,拿到的就是新创建得一个新的链接对象了。 */if(this.currentUseCount < this.maxUseCount)this.dataSource.connectionsPool.addLast(this);else {this.realConnection.close();this.dataSource.currentCount--;}}
//Connectio接口中实在是有太多的方法,在此就不写了,我们主要关注close()方法
}
为了更清楚地表达思想,我们可以慢慢来优化代码(不好的代码)
public class MyDataSource2 {private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";private static int initCount = 1;private static int maxCount = 1;int currentCount = 0;LinkedList<MyConnetion> connectionsPool = new LinkedList<MyConnetion>();public MyDataSource2() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {synchronized (connectionsPool) {if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}throw new SQLException("已没有连接");}}public void free(Connection conn) {if(conn instanceof MyConnetion) {this.connectionsPool.addLast((MyConnetion)conn);}}private MyConnetion createConnection() throws SQLException {Connection realConn = DriverManager.getConnection(url, user, password);MyConnetion myConnetion = new MyConnetion(realConn, this);return myConnetion;}
}
因为针对接口编程是面向对象的第一原则,所以我们优化代码为:
public class MyDataSource2 implements DataSource {//实现了DataSource接口之后,就是一个标准的数据源了,我们的程序只和数据源打交道,后面会讲DBCP实现private static String url = "jdbc:mysql://localhost:3306/jdbc?generateSimpleParameterMetadata=true";private static String user = "root";private static String password = "yezi";private static int initCount = 1;private static int maxCount = 1;int currentCount = 0;/** 针对接口编程——面向对象的第一原则*/LinkedList<Connection> connectionsPool = new LinkedList<Connection>();public MyDataSource2() {try {for(int i = 0; i < initCount; i++) {this.connectionsPool.addLast(this.createConnection());this.currentCount++;}} catch(SQLException e) {throw new ExceptionInInitializerError(e);}}public Connection getConnection() throws SQLException {synchronized (connectionsPool) {if(this.connectionsPool.size() > 0)return this.connectionsPool.removeFirst();if(this.currentCount < maxCount) {this.currentCount++;return this.createConnection();}throw new SQLException("已没有连接");}}public void free(Connection conn) {this.connectionsPool.addLast(conn);}private Connection createConnection() throws SQLException {/** 这是一个真实的connection*/Connection realConn = DriverManager.getConnection(url, user, password);MyConnetion myConnetion = new MyConnetion(realConn, this);return myConnetion;//返回一个代理对象}
//实现DataSource接口中一系列方法,较多,不写
}
Java的动态代理及使用该技术完善连接代理
在上面的示例中,我们为了产生一个代理对象实现了Connection接口的所有的方法,但是我们只需要修改它的close方法,别的方法我们都需要交给真正的Connection对象去处理,比较麻烦,我们用动态代理(?)来实现它。
class MyConnectionHandler implements InvocationHandler {private Connection realConnection;private Connection warpedConnection;private MyDataSource2 dataSource;/** 限制其最大使用次数*/private int maxUseCount = 5;/** 记录当前使用次数*/private int currentUserCount = 0;MyConnectionHandler(MyDataSource2 dataSource) {this.dataSource = dataSource;}Connection bind(Connection realConn) {this.realConnection = realConn;/** Proxy类就像程序员一样可写代码* Proxy写一个类,此类实现了Connection接口,* 对connection接口的方法调用转化给调用处理器MyConnectionHandler* * 动态产生warped(包裹的)Connection。*/this.warpedConnection = (Connection)Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {Connection.class}, this);return warpedConnection;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if("close".equals(method.getName())) {this.currentUserCount++;if(this.currentUserCount < this.maxUseCount)this.dataSource.connectionsPool.addLast(this.warpedConnection);else {this.realConnection.close();this.dataSource.currentCount--;}}/** 调用方法的时候如果是close方法就执行我们的逻辑(上面的代码),* 对于其他的所有的方法,全部交给真实Connection对象本身自己去处理 * 调用realConnection实例的method方法*/ return method.invoke(this.realConnection, args);} }
当写完上面的代理类后,我们还是需要修改MyDataSource2类的createConnection()方法来调用我们的代理类,将它需要的参数传递给它并把生成的代理类返回:(注意:MyDataSource2类中只须修改createConnection()),部分代码如下:
private Connection createConnection() throws SQLException {/** 这是一个真实的connection*/Connection realConn = DriverManager.getConnection(url, user, password);MyConnectionHandler proxy = new MyConnectionHandler(this);return proxy.bind(realConn);}
标准DataSource接口及数据源的总结介绍
理解数据源的优势与特点:
- DataSource用来取代DriverManager来获取Connection;
- 通过DataSource获得Connection速度很快;
- 通过DataSource获得的Connection都是已经被包裹过的(不是驱动原来的连接),他的close方法已经被修改;
- 一般DataSource内部会用一个连接池来缓存Connection,这样可以大幅度提高数据库的访问速度;
- 连接池可以理解成一个能够存放Connection的Collection;
- 我们的程序只和DataSource打交道,不会直接访问连接池。