JDBC详解

在上一篇博客中我们就使用了jdbc ,它可以连接数据库,并用java语言操作数据库,但是我们只是用它做了一下事务的演示,在这篇博客中我们来详细的谈谈JDBC;

JDBC

  • 定义:
    Java database connectivity 的缩写,即Java数据库连接接口;
    是java语言操作数据库的 api (应用程序编程接口) ,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。

    JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。

  • JDBC常用接口:

  1. Driver接口:
    Driver接口由数据库厂家提供,作为java开发人员,只需要使用Driver接口就可以了。在编程中要连接数据库,必须先装载特定厂商的数据库驱动程序,不同的数据库有不同的装载方法。
    jdbc的驱动就是一个连接工厂,生成的产品是连接对象
    com.mysql.jdbc.Driver 是Driver的mysql实现类
    oracle.jdbc.Driver 是Driver的oracle实现类
   Class.forName("驱动类名");
   例如:
   Class.forName("com.mysql.jdbc.Driver");
   Class.forName("oracle.jdbc.driver.OracleDriver");

:jdbc 3.0 以上版本都可以省略加载驱动这一步

  1. Connection 接口:
    代表了java和数据之间的通道,桥梁;
    Connection与特定数据库的连接(会话),在连接上下文中执行sql语句并返回结果;
    Connection.getConnection(url,username,password);就可以获取一个数据库的连接:
    连接MySql数据库:
Connection conn = 
DriverManager.getConnection("jdbc:mysql://host:port/database", "user", "password");

常用方法:

    createStatement():创建向数据库发送sql的statement对象。
    prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。
    setAutoCommit(boolean autoCommit):设置事务是否自动提交。
    commit() :在链接上提交事务。
    rollback() :在此链接上回滚事务。
  1. Statement 接口:
    这个语句用来执行静态SQL语句并返回它所生成结果的对象。
    可以用来执行 insert, update, delete , select . 语句
    两个Statement 常用类:
    (1). Statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。
    (2). PreparedStatement :继承自Statement接口,由preparedStatement创建,用于发送含有一个或多个参数的SQL语句。PreparedStatement对象比Statement对象的效率更高,并且可以防止SQL注入,所以我们一般都使用PreparedStatement。
    四个常用Statement方法:
    (1). executeQuery(String sql):运行select语句,返回ResultSet结果集。
    (2). executeUpdate(String sql):运行insert/update/delete操作,返回更新的行数。
    (3). addBatch(String sql) :把多条sql语句放到一个批处理中。
    (4). executeBatch():向数据库发送一批sql语句执行。

  2. ResultSet 接口
    结果集 代表的是查询的结果;
    resultSet是我们运行了查询语句之后,获得的结果集合;它还提供了对结果集进行滚动的方法;
    ResultSet.next(); 向下滚动一条记录;
    ResultSet.Previous(); 向前滚动一条记录;
    同时我们需要用特定的get方法检索不同类型字段的方法;
    resultSet.getString(int index/String columnName); 获得数据库里是varchar、char等类型的数据对象;
    这里注意它的参数,可以是index 即数据表中的列号,或者也可以是直接明确的columnName 即列名,确保获取到对应想要的列的数据;
    resultSet.getInt(int index/String columnName); 获得数据库里是 int 类型的数据对象;

JDBC操作流程

这里我们省略了加载驱动的步骤,在jdk 3,以上加载驱动的过程都可以省略;

  1. 获取连接对象
   DriverManager.getConnection(url, 用户名, 密码); 
   // 内部调用了  Driver 对象获取数据库连接

url 的格式 :
URL用于标识数据库的位置,通过URL地址告诉JDBC程序连接哪个数据库;

jdbc:mysql://ip地址:端口号/数据库名?参数

这里的参数我们后面会有解释;
例如:mysql数据库

Connection conn = 
DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
  1. 创建语句
Statement stmt = conn.createStatement();
  1. 执行sql语句
int rows = stmt.executeUpdate();
// 用来执行 insert , update, delete , DDL , 返回值代表影响行数

ResultSet rs = stmt.executeQuery(); // 用来执行 select
//boolean moreRows = rs.next() 取得下一条记录, 

while(rs.next()) {
		System.out.println(rs.getString(1))//获取第一行第一列的值;
}

从上面我们看到,executeQuery()得到的结果集调用next()方法,它的返回值是boolean类型,因此我们为了获得所有结果集中的数据,可以用while()方法,给传入的条件就是rs.next(),如果 是true ,表示有下一条记录, false 表示没有了,这样确保我们获取到所有查询到的数据;

  1. 释放资源
    注意:先打开的后关闭!
	resultSet.close();
	stat.close();
    conn.close();

例子:
我们现在数据库中有一张student表,然后我们可以用JDBC对他进行一系列操作;
JDBC详解
我们先写一个学生类:

import java.sql.Date;

public class Student {
    private int sid;
    private String sname;
    private  String birthday;
    private String sex;

    public Student(int sid, String sname, String birthday, String sex) {
        this.sid = sid;
        this.sname = sname;
        this.birthday = birthday;
        this.sex = sex;
    }

    public Student() {
    }

    public int getSid() {
        return sid;
    }

    public void setSid(int sid) {
        this.sid = sid;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }


    @Override
    public String toString() {
        return "Student{" +
                "sid=" + sid +
                ", sname='" + sname + '\'' +
                ", birthday='" + birthday + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

比如我们现在要先删除数据表中名叫 “ 李四 ”的学生的信息,然后查询获取这张学生表中的所有数据;

 private static List<Student> sqlqueryaction(Statement stat) throws SQLException {
        ResultSet resultSet = stat.executeQuery("select * from student");
        
        List<Student> list = new ArrayList<>();
        while(resultSet.next()){
            Student stu = new Student(resultSet.getInt("sid"),
                    resultSet.getString("sname"),
                    resultSet.getString("birthday"),
                    resultSet.getString("sex"));
            list.add(stu);
        }
        return list;
    }

然后我们将结果集中的数据分别获取,当作Student构造类的参数,得到Student类的对象,将对象存入一个集合中,以便我们对获取到的数据进行操作;

public class jdbcutils {
    static final String url ="jdbc:mysql://localhost:3306/test";
    static final String username = "root";
    static final String pass = "123456";
    public static Connection getconnection() throws SQLException {
        return DriverManager.getConnection(url,username,pass);
    }
}
    public static void main(String[] args) {
        try(Connection conn = jdbcutils.getconnection()){

            try (Statement stat = conn.createStatement()){
            	  int lows = 
            	  stat.executeUpdate("delete from student where sname = '李四'");
                System.out.println("影响行数为:"+lows);
                List<Student> stu = sqlqueryaction(stat);
                stu.forEach(s -> System.out.println(s));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

这里我们将获取连接的代码写成一个工具类,每次直接调用该工具类的getconnection()方法,直接获取一个连接对象;同时我们也用了try-with-resources方法,省去了释放资源的操作;这个例子也只是将获取到的结果进行了输出操作,其实我们返回了student类的数据集合,完全可以再对其进一步操作,这里我们演示就只将它输出吧;
JDBC详解

结果如上图;

PreparedStatement 预编译语句对象

PreparedStatement 能够实现sql语句的预编译,同时它可以动态的传入参数,而不像Statement那样每次传入一个固定的sql语句,这样的固定sql语句表示它只能完成特定的sql操作,而PreparedStatement 可以用 ?来占位某个参数,你动态的传入参数,就可以实现不同效果的操作了;

  1. 需要预先提供sql语句
PreparedStatement psmt = conn.prepareStatement(String sql);
  1. 可以在sql中用?占位某个值
insert into student(sid,sname,birthday,sex) values(null, ?, ?, ?)
  1. 给?赋值
//使用PreparedStatement中一系列以 set开头的方法
//setString(?的位置, 值)
//setInt(?的位置, 值)
//setDate(?的位置, 值)

psmt.setString(1, "李四");
psmt.setString(2, "1999-10-7");
psmt.setString(3,"男");
  1. 执行sql
psmt.executeUpdate();

注意: ?能够占位的只有值, 不能是表名、列名、关键字

  1. 释放资源
    注意:先打开的后关闭! 注意:先打开的后关闭!
	resultSet.close();
	stat.close();
    conn.close();

上面的操作就是我们将之前删除的李四学生重新插入回表中;

 try(Connection conn = jdbcutils.getconnection()){
    PreparedStatement psmt =
       conn.prepareStatement("insert into student(sname,birthday,sex) values(?,?,?)");
            psmt.setString(1,"李四");
            psmt.setString(2,"1999-10-7");
            psmt.setString(3,"男");
            int low = psmt.executeUpdate();
            System.out.println("数据写入完成,影响行数为:"+low);
            try(Statement stat = conn.createStatement()){
                List<Student> stu = sqlqueryaction(stat);
                stu.forEach(s -> System.out.println(s));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

JDBC详解

结果如图,我们看到李四学生已经添入student表;
其实我们不光可以添加李四学生,我们动态的传入别的学生信息,就可以实现信息的写入,这样就灵活多了;

  • PreparedStatement可以提高性能.
    每一种PreparedStatement尽最大可能提高性能.数据库都会尽最大努力对预编译语句提供最大的性能优化.
    因为预编译语句有可能被重复调用.所以语句在被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行.这并不是说只有一个Connection中多次执行的预编译语句被缓存,而是对于整个DB中,只要预编译的语句语法和缓存中匹配.那么在任何时候就可以不需要再次编译而可以直接执行.而statement的语句中,即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配.比如:
    insert into tb_name (col1,col2) values (‘11’,‘22’);
    insert into tb_name (col1,col2) values (‘11’,‘23’);
    即使是相同操作但因为数据内容不一样,所以整个个语句本身不能匹配,没有缓存语句的意义.事实是没有数据库会对普通语句编译后的执行代码缓存.

所以这种机制使得我们做相似的sql操作时能尽大可能的提高sql的执行效率;


sql注入问题

SQL注入攻击:
  由于jdbc中执行的SQL语句是拼接出来的,其中有一部分内容是由用户从客户端传入,所以当用户传入的数据中包含sql关键字时,就有可能通过这些关键字改变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’肯定成立,所以可以任何通过验证.
这就是应为用户恶意的传参,传入sql 的关键字,使得原本的语义改变了;

那我们应该如何防范sql注入呢?

  1. 对参数内存做检查,内部不能有sql关键字例如:or
  2. 用PreparedStatement,预编译 ;
    如果你使用预编译语句.你传入的任何内容就不会和原来的语句发生任何匹配的关系.只要全使用预编译语句,你就用不着对传入的数据做任何过虑.而如果使用普通的statement,有可能要做费尽心机的判断和过虑;这也是PreparedStatement方法的另外一大好处;