Java后端自顶向下方法——探索JDBC

Java后端自顶向下方法——探索JDBC

(一)什么是JDBC

学了这么久,我们终于走出了java后端的核心部分,到达了java的旁系知识点。也就是说,接下来的内容不再是java后端的专属内容了,普通java程序也可能经常会用到。

废话不多说,这次讲的JDBC,全称Java Database Connectivity,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口。既然是和数据库打交道的,那显然是很重要的,因为后端开发主要负责的就是业务和数据,业务就是指逻辑,而数据自然就是指数据库中存放的内容了,JDBC就是业务和数据之间的桥梁。

在这里插入图片描述

JDBC是用于在Java语言编程中与数据库连接的API。JDBC是一个规范,它提供了一整套接口,允许以一种可移植的访问底层数据库API。使用JDBC驱动程序来访问数据库,并用于存储数据到数据库中。简单来说,就是将访问数据库的过程进行了一次抽象,我们无需按照不同的数据库来书写不同的代码,我们只需要关心我们的业务逻辑即可。

我们编写的java应用程序通过JDBC API首先连接到JDBC Driver,这些JDBC驱动器都是由各大数据库厂家针对JDBC提供的,我们可以在网上下载jar包来使用,然后通过JDBC驱动器就能连接到我们的数据库了,非常简单快捷。有了JDBC,我们可以仅仅掌握一套API就可以对市面上常见的几乎所有的数据库进行相应操作,大大降低了我们的学习成本。

(二)如何使用JDBC

先来看一个最简单的例子:

package com.demo;

import java.sql.*;

public class JDBCTest {
    
    
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
    
    
        Class.forName("com.mysql.jdbc.Driver");

        String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
        String userName = "root";
        String password = "19710825@Apple";
        Connection connection = DriverManager.getConnection(url, userName, password);

        String sql = "select * from testtable";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        ResultSet resultSet = preparedStatement.executeQuery();
        while(resultSet.next()){
    
    
            System.out.println(resultSet.getString("name"));
        }
        preparedStatement.close();
        connection.close();
    }
}

首先我们要了解使用JDBC的流程。首先我们需要加载驱动类,然后要通过DriverManager创建Connection对象,Connection对象的创建需要三个参数:连接字符串、数据库用户名和密码。接着书写我们需要的SQL语句,通过Connection对象拿到preparedStatement对象,然后执行SQL,通过返回值就能拿到我们需要的数据了。最后还不要忘了关闭preparedStatement和Connection,释放资源。

从此我们就可以很清楚的看出JDBC带来的便捷性,比如我们现在突然要换一个数据库,但原有的业务逻辑不变。换句话说,就是我们的Connection对象发生了改变(不同的数据库的Connection肯定是不一样的),从代码上看,我们只需要修改驱动类、连接字符串、用户名和密码,也就是所有与Connection有关的东西需要发生变化,但是我们的业务逻辑完全不需要做任何修改就能满足我们的需要。

我们可以发现一个细节,如果我们需要执行的SQL非常多,那我们的Connection对象一直反复在创建、释放的过程中,这会造成很大的资源开销,这对我们的应用程序的性能是非常不利的。因此我们就要想方法去改变,因此孕育出了数据库连接池这种技术(由此可见任何技术的产生都是有原因的,他一定是为了解决开发过程中某种具体的问题),具体细节我会在其他的文章中讲,在这里就暂不赘述。

(三)为何要用preparedStatement

我们上面的代码中用到了preparedStatement,其实他是从Statement继承过来的,是他的子类。那为什么我们不用Statement而尽量要用preparedStatement呢?

首先我们可以从java面向对象的继承关系中来看这个问题,众所周知,子类会继承父类的所有方法(final除外),同时也可以有自己的方法,也就是这些自己的方法给了子类比父类更强大的功能,有点青出于蓝而胜于蓝、长江后浪推前浪的感觉。

那么问题来了,preparedStatement究竟多了哪些功能和特性呢?大概有这么三项:

  1. 提高执行语句的性能
  2. 可读性和可维护性更好
  3. 具有更高的安全性

下面我们来说第一点,性能好。PreparedStatement可以使用占位符,并且是预编译的,批处理比Statement效率高。我们的SQL语句可以这样写:

String sql = "update user set username = ? where id = ?";

其中的?就代表占位符,大家还记得String.format()方法里面的占位符%s、%d这类的吧,或者是C语言中的printf()函数中的占位符,他们都是几乎一样的作用,就是占个位置,再把相应的值填充进去。

按照文章开头写的步骤,下一步就是:

PreparedStatement preparedStatement = connection.prepareStatement(sql);

这里就是我们要讲的预编译,我们可以看出来我们将SQL进行了传参,注意,这时候我们还没有给占位符填充需要的值,所以叫预编译,也可以看做一个SQL语句的“模板”,我们向里面填充不同的值,就能形成不同的SQL语句。这也就是PreparedStatement在批处理时性能更好的原因。就像我们平时做东西,如果有了模板,我们就能够很快速按照我们的实际需求做出我们想要的东西(典型案例:PPT模板、论文模板)。

预编译结束之后我们就需要给占位符赋值。赋值也非常简单:

preparedStatement.setString(1, "jack");
preparedStatement.setInt(2, 15);

参数中的第一个参数分别是1和2,它代表的是第几个问号的位置。如果sql语句中只有一个问号,那就不用声明这个参数。第二个参数代表我们要赋的值。因此,借助模板和填充值,我们获得了这样一条SQL语句:

update user set username = "jack" where id = 15

这也就是我们上面写的第二个特点:可读性和可维护性更好。有了模板,我们可以抛开数值而专注于逻辑,并且模板也使得代码能够重用,如果需要修改逻辑也非常方便。由此我们可以发现,我们开发时需要注意从具体到抽象,从实体中抽象出特征是每一个开发者必备的技能。

最后我们来说第三点,安全性高。使用PreparedStatement能够预防SQL注入攻击,所谓SQL注入,指的是通过把SQL命令插入到Web表单提交或者输入域名或者页面请求的查询字符串,最终达到欺骗服务器,执行恶意SQL命令的目的。SQL注入非常巧妙,一般都是通过注入一个永真的语句来使SQL的某个条件语句为真。如果我们使用Statement,由于不能使用占位符,为了达到代码重用的目的,我们一定会用到字符串拼接,例如:

String sql = "select * from user where name = " + x + "and password = " + y;

在这个语句中x和y为变量,可能有人觉得,这不和之前的占位符差不多嘛,我们可以根据需要设置x和y的值。但是,这其中蕴藏着巨大隐患。假设这是一个登陆功能,有两个输入框,输入的结果会传给x和y,我们需要输入正确的用户名和密码才能获取用户的信息。黑客不知道用户的密码,但他可以这样给x和y赋值:

select * from user where name = 'Tom' and password = '123456' or 1 = 1

很显然,黑客在一个输入框中输入了Tom,另一个输入了’123456’ or 1 = 1。仔细观察,黑客这样一顿神操作之后,密码的判断条件直接被这个1 = 1弄得形同虚设,黑客随便输一个密码就可以使这个判断条件成立,从而获取到数据!

为什么preparedStatement可以天生免疫SQL注入呢?我们来看看上面注入东西,也及时’123456’ or 1 = 1。很显然,黑客通过引号截断了语句,使得SQL中增加了一段语句,就是or 1 = 1这段,这段语句显然是不该出现的。那我们如何避免呢?本来是想输入一个字符串,结果居然变成了一段字符串加一段SQL语句!最容易想到的,我们将这段注入外面套上引号,并且将内部的所有引号转义,就像"\‘123456\’ or 1 = 1",这样就变成了一个整体的字符串,也就没法再进行搞破坏了。没错,preparedStatement就是这么干的!

因此,只是因为这一个可能出现的的隐患,就足够可以让我们完全弃用Statement。不过有一个条件下我们还是能使用Statement的,那就是我们的SQL语句不需要重用,也就是没有字符串拼接的时候。不过,Statement不能实现的preparedStatement可以实现,Statement能实现的preparedStatement也能实现。那我们为什么还要用Statement呢?你说对吧?

PS:注意!当使用preparedStatement时,有一种SQL注入无法被预防(我也是最近听人说的),在这里我做一个关键提示:模糊查询中的%。大家可以按照这个方向研究一下。不过这个问题不是特别关键,因为他很难对数据库的数据造成破坏。

(四)JDBC与事务

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列。具体的介绍我会在未来的《数据库系统原理》专题中介绍,这里主要是告诉大家怎么用。没听说过事务的朋友可以略过这一段或者上网了解一下事务的特征。

你可能会很疑惑,事务难道不需要提交吗?不需要回滚吗?为什么上面的代码都没有体现出来?因为JDBC中,事务是默认提交的,因此我们如果需要自定义事务,我们先得把这个设置关了。

然后,如果SQL语句全都正确执行,就需要提交事务。如果出错,我们就要将事务回滚。最后,无论事务是提交还是回滚,我们都要关闭数据库连接。这样就形成了我们的一个事务的基本模板:

try {
    
    
    // 关闭自动提交:
    connection.setAutoCommit(false);
    // 执行多条SQL语句:
    ......(省略)
    // 没有异常,提交事务:
    connection.commit();
} catch (SQLException e) {
    
    
    // 遇到异常,回滚事务:
    connection.rollback();
} finally {
    
    
    // 关闭连接:
    connection.close();
}

开启事务的关键代码是connection.setAutoCommit(false),表示关闭自动提交。提交事务的代码在执行完指定的若干条SQL语句后,调用connection.commit()。要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获异常并调用connection.rollback()回滚事务。最后,在finally中关闭数据库连接。

实际上,默认情况下,我们获取到Connection连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,这也是为什么前面我们没有写commit的原因:因为默认有这种“隐式事务”。只要关闭了Connection的autoCommit,那么就可以在一个事务中执行多条语句,事务以commit()方法结束。

2020年6月17日

猜你喜欢

转载自blog.csdn.net/weixin_43907422/article/details/106390816
今日推荐