jdbc原理详解

最近做了一个切换数据源的功能,发现自己对mybatis以及数据库连接底层原理了解不够,特此先写篇关于jdbc文章增加自己的深度

先入一个基本的代码,用jdbc连接数据库

            private String url="jdbc:mysql://localhost:3306/表名";
   			private String name="数据库名称";
    		private String pwd="数据库密码";
          1、  Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(url,user,password);
            String sql = "update user set username=? where id = ?";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
			//参数中的第一个参数分别是1和2,它代表的是第几个问号的位置。如果sql语句中只有一个问号,那就不用声明这个参数。
            preparedStatement.setString(1,"Fant.J");
            preparedStatement.setInt(2,27);

//执行executeQuery方法得到结果
ResultSet resultSet = preparedStatement.executeQuery();
			//将结果进行输出
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String birthday = resultSet.getString("birthday");
                String sex = resultSet.getString("sex");
                String address = resultSet.getString("address");

                System.out.println("  " + username + " " + birthday + " " + sex
                        + " " + address);
            }

重点方法Class.forName(“com.mysql.jdbc.Driver”);
如果去掉这一句会有以下异常:
java.sql.SQLException: No suitable driver found for xxx….
这个方法涉及到了class的加载,Class.forName(String className)这个方法会做以下几件事情:
1、装载。将字节码读入内存,并产生一个与之对应的java.lang.Class类对象
2、连接。这一步会验证字节码,为static变量分配内存,并赋默认值(0或null),并可选的解析符号引用(这里不理解没关系)
3、初始化。为类的static变量赋初始值,假如有static int a = 1;这个将a赋值为1的操作就是这个时候做的。除此之外,还要调用类的static块。(这一步是要点)
 Class.forName(String className)方法会将这三步都做掉
而在jdbc4以后已经不需要显式的调用Class.forName了,调用getConnection的时候DriverManager会自动去加载合适的驱动。

PreparedStatement和Statement

PreparedStatement是Statement的子类
1、PreparedStatement是预编译的,对于批量处理可以大大提高效率. 也叫JDBC存储过程
2、使用 Statement 对象。在对数据库只执行一次性存取的时侯,用 Statement 对象进行处理。PreparedStatement 对象的开销比Statement大,对于一次性操作并不会带来额外的好处。
3、statement每次执行sql语句,相关数据库都要执行sql语句的编译,preparedstatement是预编译得, preparedstatement支持批处理
解释:PreparedStatement表示预编译的 SQL 语句的对象,而Statement用于执行静态 SQL 语句并返回它所生成结果的对象。即PreparedStatement可以像前面代码所说的,将执行sql需要的参数作为object传给PreparedStatement方法里去执行,俩者区别如下:

		//PreparedStatement执行方法
 String sql="insert into lover values(?,?,?)";
			 ps=conn.prepareStatement(sql);
			 //4、执行sql语句
			 ps.setInt(1,21);//代表设置给第一个?号位置的值为Int类型的21
			 ps.setString(2,"suwu150");//代表设置给第二个?号位置的值为String类型的suwu150
			 ps.execute();//执行

		//Statement执行方法
		stat=conn.createStatement();
             //4、执行sql语句
             String sql="insert into lover values(22,'suxingxing',to_date('21-9-2016','dd-mm-yyyy'))";         
/          stat.execute(sql);

可以看到PreparedStatement具有更灵活的使用方式,且复用性更高

SQL注入:Statement执行语句将数据插入到表中的时候,会原封不动的将用户写的内容拼接在SQL中,此时可能会发生对数据库的意外的损坏,甚至数据泄露
恶义SQL语法:
String sql = “select * from tb_name where name= '”+varname+"’ and passwd=’"+varpasswd+"’";
如果我们把[’ or ‘1’ = ‘1]作为varpasswd传入进来.用户名随意,看看会成为什么?
将执行:
select * from tb_name = ‘随意’ and passwd = ‘’ or ‘1’ = ‘1’;
因为’1’=‘1’肯定成立,所以可以任何通过验证.更有甚者:
把[’;drop table tb_name;]作为varpasswd传入进来,则:
select * from tb_name = ‘随意’ and passwd = ‘’;drop table tb_name;
有些数据库是不会让你成功的,但也有很多数据库就可以使这些语句得到执行.

而如果你使用预编译语句.你传入的任何内容就不会和原来的语句发生任何匹配的关系.只要全使用预编译语句,你就用不着对传入的数据做任何过虑.而如果使用普通的statement,有可能要对drop,;等做费尽心机的判断和过虑。即当预编译只有俩个参数的时候,不会识别成3个参数,它将第2个参数进行识别,如果不匹配其类型则发生报错,不再可能出现类似于第3个参数是[’ or ‘1’ = '1]通过第2个参数传进来的情况

PreparedStatement复用性代码体现:

			//一次实例化,多次使用
            preparedStatement.setString(1,"Fant.J");
            preparedStatement.setInt(2,27);
			//第一次执行
            preparedStatement.executeUpdate();
            
            preparedStatement.setString(1,"Fant.J reUseTest");
            preparedStatement.setInt(2,27);
            //改变参数第二次执行
            preparedStatement.executeUpdate();
			//改变sql第三次执行
 			String sql2 = "select * from user";
            ResultSet resultSet = preparedStatement.executeQuery(sql2);


PreparedStatement对象不仅包含了SQL语句,而且大多数情况下这个语句已经被预编译过,因而当其执行时,只需DBMS运行SQL语句,而不必先编译。当你需要执行Statement对象多次的时候,PreparedStatement对象将会大大降低运行时间,当然也加快了访问数据库的速度。sql查询细节可以看下另一篇文章MySQL之Sql语句的执行原理

这里就不讲细节了,我们只关注最关键的部分。当一个数据库收到一个statement后,数据库引擎会先解析statement,然后检查其是否有语法错误。一旦statement被正确的解析,数据库会选出执行statement的最优途径。遗憾的是这个计算开销非常昂贵。数据库会首先检查是否有相关的索引可以对此提供帮助,不管是否会将一个表中的全部行都读出来。数据库对数据进行统计,然后选出最优途径。当决创建查询方案后,数据库引擎会将它执行。存取方案(Access Plan)的生成会占用相当多的CPU。理想的情况是,当我们多次发送一个statement到数据库,数据库应该对statement的存取方案进行重用。如果方案曾经被生成过的话,这将减少CPU的使用率。

PreparedStatement有两个潜在的重用(复用)级别
1、JDBC驱动程序重新使用PreparedStatement(代码级别,如上面代码重用PreparedStatement对象)。
2、数据库重用PreparedStatement(数据库级别)。
首先,JDBC驱动程序可以在内部缓存PreparedStatement对象,从而重用PreparedStatement对象。这可能会节省一小部分PreparedStatement创建时间。

Statement Caches
数据库已经具有了类似的功能。它们通常会用如下方法对statement进行缓存。使用statement本身作为key并将存取方案存入与statement对应的缓存中。这样数据库引擎就可以对曾经执行过的statements中的存取方案进行重用。举个例子,如果我们发送一条包含SELECT a, b FROM t WHERE c = 2的statement到数据库,然后首先会将存取方案进行缓存。当我们再次发送相同的statement时,数据库会对先前使用过的存取方案进行重用,这样就降低了CPU的开销。
在这里插入图片描述
举例:我这里使用了整个statement为key。也就是说,如果我们发送一个包含SELECT a, b FROM t WHERE c = 3的statement的话,缓存中会没有与之对应的存取方案。这是因为“c=3”与曾经被缓存过的“c=2”不同。所以

for (int i = 0; i < 1000; i++)  {
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = " + i);
ResultSet rs = Ps.executeQuery();
rs.close();
ps.close();
}

在这里缓存不会被使用,因为每一次迭代都会发送一条包含不同SQL语句的statement给数据库。并且每一次迭代都会生成一个新的存取方案。现在让我们来看看下一段代码:

PreparedStatement ps = conn.prepareStatement("select a,b from t where c = ?");
for (int i = 0; i < 1000; i++)  {
ps.setInt(1, i);
ResultSet rs = ps.executeQuery();
rs.close();
ps.close();
}

这样就具有了更好的效率,这个statement发送给数据库的是一条带有参数“?”的SQL语句。这样每次迭代会发送相同的statement到数据库,只是参数“c=?”不同。这种方法允许数据库重用statement的存取方案,这样就具有了更好的效率。这可以让你的应用程序速度更快,并且使用更少的CPU,这样数据库服务器就可以为更多的人提供服务。

PreparedStatement与J2EE服务器

当我们使用J2EE服务器时事情会变的比较复杂。通常,一个perpared statement会同一个单独的数据库连接相关联。当数据库连接被关闭时prepared statement也会被丢弃。通常,一个胖客户端会获取一个数据库连接并将其一直保持到退出。它会用“饿汉”(eagerly)或“懒汉”(lazily)方式创建所有的parepared statements。“饿汉”方式会在应用启动时创建一切。“懒汉”方式意味着只有在使用的时候才去创建。“饿汉”方式会使应用程序在启动的时候梢有延迟,但一旦启动后就会运行的相当理想。“懒汉”方式使应用程序启动速度非常快(但不会做任何准备工作),当需要使用prepared statement的时候再去创建。这样,在创建全部statement的过程中,性能是非常不稳定的,但一旦创建了所有statement后,它会像“饿汉”式应用程序一样具有很好的运行效果。请根据你的需要来选择最好的方式,是快速启动?还是前后一致的性能。

J2EE应用的问题是它不会像这样工作,连接只会在请求期间被保持。那意味着必须每一次请求的时候都创建prepared statement。这远没有胖客户端那种一直保持prepared statement的执行性能好。J2EE厂商已经注意到了这个问题,并且提供了连接池(ConnectionPool)以避免这种问题。

当J2EE服务器提供了一个连接给你的应用程序时,其实它并没有给你真正的数据库连接,你只是获得了一个包装器(Wrapper)。你可以去看看你所获得的连接的类名以证实这一点。它并不是一个JDBC连接,而是一个由应用服务器创建的类。所有的JDBC操作都会被应用服务器的连接池管理器所代理。所有的JDBC ResultSets,statements,CallableStatements,preparedStatements等都会被包装并以一个“代理对象”(Proxy Object)的形式返回给应用程序。当你关闭了连接,这些对象会被标记为失效,并被垃圾回收器所回收。

通常,如果你对一个数据库连接执行close,那这个连接会被JDBC驱动程序关闭。但我们需要在J2EE服务器执行close的时候数据库连接会被返回连接池。我们可以创建一个像真正的连接一样的JDBC Connection代理类来解决这个问题。它有一个对真正连接的引用。当我们执行一个连接上的方法时,代理会将操作转给真正的连接。但是,当我们对一个连接执行close时,这个连接并不会关闭,而是会送回连接池,并可以被其他请求所使用。一个已被准备过的prepared statement也会因此而得到重用。

J2EE服务器的连接池管理器也实现了缓存的使用。J2EE服务器保持着连接池中每一个连接准备过的prepared statement列表。(缓存适用于整个数据库,如果你安排所有的应用程序使用相同的参数化SQL语句,然后你的其他应用程序就可以重用被准备过的prepared statement。这是应用服务器的一个优势,因为所有的数据库操作都集中在数据库操作层,注意这个说法是建立在jdbc层面上,将jdbc看成一个黑箱,而不管去如何实现preparedStatement的创建)当我们在一个连接上调用preparedStatement时,应用服务器会检查这个statement是否曾经准备过。如果是,这个PreparedStatement会被返回给应用程序。如果否,调用会被转给JDBC驱动程序,然后将新生成的statement对象存入连接缓存。

每个连接都有一个缓存的原因是因为:JDBC驱动程序就是这样工作的。任何prepared statement都是由指定的连接所返回的。

如果我们想利用这个缓存的优势,那就如前面所说的,使用参数化的查询语句可以在缓存中找到曾经使用过的statement。大部分应用服务器允许你调整prepared statements缓存的大小。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值