JavaWeb学习——JDBC学习

163 阅读9分钟

JDBC学习

JDBC概念:

  • JDBC就是使用Java语言操作关系型数据库的一套API
  • 全称(Java DataBase Connectivity) Java数据库连接

JDBC本质/由来:

  • 在初始的时候,考虑到代码要对各种不同的关系型数据库进行操作,但是关系型数据库几十上百个,那这样的话很大程度上意味着我们对于不同的数据库完完全全要写不同的代码。
  • 这时官方(sun公司)定义来了一套操作所有关系型数据库的规则,也就是一套标准的接口
  • 交由各个数据库厂商(比如mysql,oracle等等)去实现这一套接口,提供数据库驱动jar包
  • 我们可以使用这套JDBC接口编程,实际上真正执行的代码是驱动jar包中的实现类(比方说针对mysql数据库,很多时候我们称其为mysql驱动)

JDBC好处:

  • 各数据库厂商使用的是相同的接口,Java代码不需要针对不同的数据库分别开发
  • 可以随时替换掉底层数据库,访问数据库的Java代码基本不变

基本代码:

package com.nylonmin.JDBC;
​
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
​
@SuppressWarnings("all")
public class JDBCdemo {
    public static void main(String[] args) throws Exception{
        Class.forName("com.mysql.jdbc.Driver");
        String url="jdbc:mysql://127.0.0.1:3306/learnjdbc";
        String username = "root";
        String password = "123456";
        Connection connection = DriverManager.getConnection(url, username, password);
        String sql ="update students set name='小明明' where  id=1 ";
        Statement statement = connection.createStatement();
        int i = statement.executeUpdate(sql);
        System.out.println(i);  // 输出结果为1 表示有一行数据被影响到(修改到)
        statement.close();
        connection.close();
    }
}
​

JDBC-API详解:DriverManager

注册驱动

实际上就是针对这一行代码:

Class.forName("com.mysql.jdbc.Driver");

这里使用的是反射的技术,加载Driver这个类,但是只是通过这一行代码我们并不能够直观看到Driver被创建,因为只是加载这个Class。实际上,看底层代码:

类内部写了一个静态代码块,这里又引出另一个知识点,就是类中的各种成员它的加载顺序。这里就直接说结论,一个类被加载时,其内部各项成员的加载顺序如下:  也就是说,类中静态的相关东西,都和类一块儿被加载

  1. 父类静态成员变量:首先初始化父类中的静态成员变量。
  2. 父类静态代码块:接着执行父类中的静态代码块。
  3. 子类静态成员变量:然后初始化子类中的静态成员变量。
  4. 子类静态代码块:接着执行子类中的静态代码块。
  5. 父类构造函数:当创建子类对象时,首先调用父类的构造函数。
  6. 子类构造函数:在父类构造函数之后,调用子类的构造函数。

也就是说在通过反射来获取这个类的时候,同步就把驱动给注册了。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
​
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

获取数据库连接:

Class.forName("com.mysql.jdbc.Driver");
String url="jdbc:mysql://127.0.0.1:3306/learnjdbc";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);

这里引出一个很奇怪的问题,那就是,我们上面注册驱动的时候,确实将驱动注册了。但是下面DriverManager.getConnection()只是简简单单调用了一个静态方法,它是怎么获取到我上面注册的驱动呢?

这里还是得回到上面注册驱动的源码DriverManager.registerDriver(new Driver());

  • 下面代码registerDriver()方法是属于DriverManager类的一个静态方法
  • 看下面代码中英文的注释实际上也能够明白,就是把我们注册的驱动放到它的List
  • 看源码已经能看出来了吧,ListregisterdDrivers 。继续追源码,它是啥呢? private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 它是DriverManager类的一个静态成员变量!!!
  • ▲ 所以后面的事儿就不必多说,DriverManager.getConnection(url, username, password); 在这个getConnection方法中肯定就是遍历其注册的驱动列表List,寻找能够处理给定 URL 的驱动程序。如果匹配上了,就建立连接。
public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
​
        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }
​
        println("registerDriver: " + driver);
​
    }

JDBC-API详解:Connection

功能

  1. 获取执行SQL的对象

    • 普通执行SQL对象Statement statement = conn.createStatement();

    • 预编译SQL的执行SQL对象:防止SQL注入

      • PreparedStatement preparedStatement = conn.prepareStatement(sql);
    • 执行存储过程的对象 (用的不多

      • CallableStatement prepareCall(sql)
  2. 事务管理: 实际上就是对应到MySQL的事务管理

    • MySQL本身的事务管理

      • 开启事务: BEGIN;/ START TRANSACTION

        提交事务:COMMIT

        回滚事务:ROLLBACK

        一般情况下: MySQL默认自动提交事务

    • JDBC事务管理: Connection接口中定义了3个对应的方法

      • 开启事务: setAutoCommit(boolean autoCommit) true为自动提交事务,false为手动提交事务,也就是开启事务

        提交事务:commit()

        回滚事务:rollback()

直接执行以下代码,可以看到专门加了个错误 int temp = 1/0; 在执行两条sql语句之间。一旦报错,就会在catch代码块中执行事务回滚语句,最终的结果就是无论是id=1还是id=2 ,其姓名都没有被改变。

package com.nylonmin.JDBC;
​
import java.sql.*;
​
@SuppressWarnings("all")
public class JDBCdemo {
    public static void main(String[] args) throws Exception{
        Class.forName("com.mysql.jdbc.Driver");
        String url="jdbc:mysql://127.0.0.1:3306/learnjdbc";
        String username = "root";
        String password = "nylonmin747599";
        Connection conn = DriverManager.getConnection(url, username, password);
        String sql1 ="update students set name='小明明' where  id=1 ";
        String sql2 ="update students set name='小红红' where  id=2 ";
        Statement statement = conn.createStatement();
​
        try {
            conn.setAutoCommit(false); //开启事务
            //执行sql
            int i1 = statement.executeUpdate(sql1);
            System.out.println(i1);
            int temp = 1/0;
            int i2 = statement.executeUpdate(sql2);
            System.out.println(i2);
            //提交事务
            conn.commit();
        } catch (Exception e) {
            //回滚事务
            conn.rollback();
            throw new Exception(e);
        }
        stetement.close();
        conn.close();
    }
}
​

JDBC-API详解:Statement

Statement的作用:执行SQL语句

一般会调用两个方法来针对不同类型的sql语句

  • int executeUpdate(sql) :执行增加、删除、修改语句

    • 返回值: 增加、修改语句影响的行数。 删除语句执行后,执行成功也有可能返回0
  • ResultSet executeQuery(sql) 执行查询语句。

    • 返回值:ResultSet结果集对象
package com.nylonmin.JDBC;
​
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
​
@SuppressWarnings("all")
public class JDBCdemo2 {
    public static void main(String[] args) throws Exception {
        //加载驱动
        Class.forName("com.mysql.jdbc.Driver");
        //通过驱动建立连接
        //需要url  用户名 以及密码
        String url = "jdbc:mysql://127.0.0.1:3306/learnjdbc";
        String username = "root";
        String psw = "123456";
        Connection connection = DriverManager.getConnection(url, username, psw);
        String sql = "select * from students where name like '张%'";
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);
        while(resultSet.next()){
            System.out.println(resultSet.getString(2));
        }
        resultSet.close();
        statement.close();
        connection.close();
    }
}
​

JDBC-API详解:ResultSet

ResultSet(结果集对象)作用:

  • 封装了DQL查询语句的结果 ResultSet stmt.executeQuery(sql); 执行查询语句,返回ResultSet对象

获取查询结果:

  • boolean next() 将光标从当前位置向下一栋一行,并且判断当前行是否是有效行

    • 返回值 true: 有效行,当前行有数据
    • false:无效行,当前行没有数据
  • xxx getXxx(参数) 获取数据

    • xxx 数据类型,比如 int getInt(参数) String getString(参数)

    • 其形参可以为:

      • int 列的编号,从1开始
      • String列的名称

实际上这里就有集合的迭代器iterator那味儿了,迭代器iterator也是类似,通过hasNext()来进行判断。 也就是说—— 它俩的指针,实际上一开始指向的就是实际第一个数据的前一位数据。

JDBC-API详解:PreparedStatement

实际上对于执行sql语句,上面已经有了Statement,那么这里为什么仍然要创造出这么一个PreparedStatement呢?

实际上这里是为了避免sql注入的问题

什么叫做sql注入?

sql注入 说白了就是某些人通过非法输入一些字符or字符串来恶意篡改sql语句的语义。这可能是后端代码对于sql语句使用的是字符串拼接导致的

假设有一个简单的登录表单,后端的SQL查询可能是这样的:

sql

SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'";

如果攻击者输入 usernameadmin' OR '1'='1password 为任意值,SQL查询将变为:

sql

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '';

这个查询将返回所有用户的记录,因为 OR '1'='1' 总是为真。

PreparedStatement 防止 SQL 注入的几个关键点:

  • PreparedStatement 中,使用占位符(如 ?)来表示输入参数的位置,而不是将参数直接拼接到 SQL 语句中。这意味着 SQL 语句的结构在发送到数据库之前就已经确定,不会根据输入参数的变化而改变。
  • PreparedStatement 要求明确指定每个参数的数据类型,这有助于确保只有正确类型的数据被传递给数据库,减少了类型错误和相关的注入攻击。
  • 由于参数不是通过字符串拼接添加到 SQL 语句中的,攻击者无法通过输入来改变 SQL 语句的结构或逻辑。实际上最最主要的就是,将敏感字符进行了转义
package com.nylonmin.JDBC;
​
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
​
@SuppressWarnings("all")
public class JDBCdemo3 {
    public static void main(String[] args) throws  Exception{
        //获取驱动、注册驱动
        Class.forName("com.mysql.jdbc.Driver");
        //连接数据库
        String url ="jdbc:mysql://127.0.0.1:3306/learnjdbc";
        String username = "root";
        String psw = "nylonmin747599";
        //建立连接
        Connection conn = DriverManager.getConnection(url, username, psw);
        String sql = "select * from students where name like ? and score >?";
        //为上面的sql语句,创建PreparedStatement对象
        PreparedStatement preparedStatement = conn.prepareStatement(sql);
        //分别测试sql语句中问号占位符的值
        preparedStatement.setString(1, "小%");
        preparedStatement.setInt(2, 80);
        //执行查询语句,前面实例化PreparedStatement的时候已经把sql放进去了,这里就不用
        ResultSet resultSet = preparedStatement.executeQuery();
        while(resultSet.next()){
            System.out.println(resultSet.getString("name"));
        }
        preparedStatement.close();
        conn.close();
    }
}

数据库连接池

数据库连接池简介:

  • 数据库连接池是个容器,负责分配、管理数据库连接
  • 它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个

说的有点抽象了,实际上就是

如果没有数据库连接池,那么原来是怎么做的呢?原来就是来一个连接请求,就建立数据库连接,用完了之后再释放掉;如果再次之后又来一个连接请求(甚至有可能是相同的连接请求),就又会重新建立数据库连接,用完了之后又被释放掉。

▲ 有了数据库连接池之后呢(实际上数据库连接池就是一个容器),在创建的时候,就会提前申请很多个数据库的连接,提前把链接创建好。然后来一个人,就拿一个连接给他,用完了之后再重新放入到池子里面。

▲那么,如果数据库连接池中的连接这会儿都被“拿完了”呢?并且此时又来了一个新的人要连接数据库。

此时,数据库连接池就会启用一个机制—— 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。