JDBC学习记录2-API详解

JDBC学习记录2 - API详解

2024年7月9日18:08:13
上接JDBC学习记录1-快速入门

三、JDBC API详解

1. DriverManager

DriverManager(驱动管理类)两个作用:

  • 注册驱动
  • 获取数据库连接

其中有两个静态方法十分重要:getConnection()registerDriver()

其中registerDriver()在Driver的一个静态代码块里,所以真正注册驱动的是registerDriver(),在驱动类被加载进内存时候这个静态方法就自动执行了,完成了驱动的注册。

image-20240709000311093

前面是MySQL协议jdbc:mysql,后面跟着的是IP地址(127.0.0.1也可以写为localhost),接着是端口号数据库名称,最后是参数列表?参数1&参数2&...
image-20240709000410897

2. Connection

**Connection(数据库 连接/会话 对象)**的作用:

  • 获取执行SQL的对象(会在后面重点学习)

    image-20240709001050670

  • 管理事务

    image-20240709001128641

    复制第一个Demo类,来测试事务管理的功能,若现在有两个SQL原子性语句需要并发执行,要么同时执行成功要么同时失败,那么我们需要开启事务、提交事务。若出现异常,我们需要回滚事务,利用Java的try/catch语句来处理异常,快捷键Ctrl+Alt+T将代码段使用try/catch语句包围起来。

    要注意的是同一个Statement对象能执行两个Update语句,而不能执行两个Query语句,需要有两个Statement对象。

    思考:在一个事务里的两个查询语句的ResultSet对象的声明位置和内存释放问题

    若是在try/catch语句块外声明,不能保证正确回滚,若在tyr语句块里面声明,那么释放也放在try语句块里又是否不妥?

3. Statement

只有一个功能:执行SQL语句

有两个方法需要重点学习:

***DDL(Data Definition Language)***语句: 数据定义语言,主要是进行定义/改变表的结构、数据类型、表之间的链接等操作。常用的语句关键字有 CREATE、DROP、ALTER 等。

***DML(Data Manipulation Language)***语句: 数据操纵语言,主要是对数据进行增加、删除、修改操作。常用的语句关键字有 INSERT、UPDATE、DELETE 等。

***DQL(Data Query Language)***语句:数据查询语言,主要是对数据进行查询操作。常用关键字有 SELECT、FROM、WHERE 等。

***DCL(Data Control Language)***语句: 数据控制语言,主要是用来设置/更改数据库用户权限。常用关键字有 GRANT、REVOKE 等。
一般人员很少用到DCL语句。

  • int executeUpdate(sql); 执行DDL、DML语句

  • ResultSet executeQuery(sql); 执行DQL语句

  • boolean execute(sql); 可执行任意语句,返回的bool值表示是否返回ResultSet对象

    屏幕截图 2024-07-09 003713

创建一个新类Demo3_Statement用以测试,

package com.niko.jdbc;

import org.junit.Test;
import java.sql.*;

/*
 *  JDBC API 详解 Statement
 * */
public class JDBCDemo3_Statement {

    /*
    * 执行DML语句
    * */
    @Test
    public void testDML() throws Exception {
        //1.注册驱动
        Class.forName("com.mysql.jdbc.Driver");

        //2.获取连接
        String url="jdbc:mysql://localhost:3306/db?useSSL=false";
        String username="root";
        String password="1234";
        Connection conn = DriverManager.getConnection(url, username, password);

        //3.定义sql
        String sql = "update houselist set price=1400 where houseid =17";

        //4.获取执行sql的对象 Statement
        Statement stmt = conn.createStatement();

        //5.执行sql
        int count =stmt.executeUpdate(sql); //返回值是受影响的行数
        //ResultSet rs = stmt.executeQuery(sql);

        //6.处理结果
        if(count>0){
            System.out.println("success");
        }else {
            System.out.println("fail");
        }
        //System.out.println(count);

//        while (rs.next()) {
//            int id = rs.getInt("houseid");
//            String address = rs.getString("address");
//            System.out.println("ID: " + id + ", Address: " + address);
//        }

        //7.释放资源
        stmt.close();
        conn.close();
    }

    /*
     * 执行DDL语句
     * */
    @Test
    public void testDDL() throws Exception {
        //1.注册驱动
        Class.forName("com.mysql.jdbc.Driver");

        //2.获取连接
        String url="jdbc:mysql://localhost:3306/db?useSSL=false";
        String username="root";
        String password="1234";
        Connection conn = DriverManager.getConnection(url, username, password);

        //3.定义sql
        String sql = "CREATE database db2";

        //4.获取执行sql的对象 Statement
        Statement stmt = conn.createStatement();

        //5.执行sql
        int count =stmt.executeUpdate(sql); //执行完DDL语句,成功后返回值也可能是0
        //ResultSet rs = stmt.executeQuery(sql);

        //6.处理结果
//        if(count>0){
//            System.out.println("success");
//        }else {
//            System.out.println("fail");
//        }
          System.out.println(count);

//        while (rs.next()) {
//            int id = rs.getInt("houseid");
//            String address = rs.getString("address");
//            System.out.println("ID: " + id + ", Address: " + address);
//        }

        //7.释放资源
        stmt.close();
        conn.close();
    }
}

可以看到编写了两个测试单元,分别用于测试DML和DDL。

需要注意的是在编写测试单元的过程中可能会出现@Test报错的情况,不要慌张,从本地的Maven仓库导入Junit的 jar包 即可,导入方法为文件->项目结构->项目设置->库->点“+”号,如下,我导入的版本是4.12。

另外把JDBC操作语句的错误抛出,抛出的错误范围大点,设置的是Exception。

image-20240709153926401

接着运行程序,查询数据库能够发现对应的操作成功实现了。

4. ResultSet

**ResultSet(结果集对象)**的作用:

  • 封装了DQL查询语句的结果
  • 提供了一些方法用以获取查询结果。可以粗略地分为如下两类。

image-20240709154553798

要注意:ResultSet 列的编号(column index)从1开始!!!

JDBC API 案例

以后的应用场景常有:

首先从数据库中查询数据,把这些散着的数据使用Java对象封装起来,这些对象不是直接用来放到网页中去的,需要存储到容器中去,而集合又是专门用来存放对象的容器,再把集合交给对应的页面,页面再通过遍历集合等等操作,实现各种各样的显示。

image-20240709162814304

下面是具体操作步骤:

  • 首先需要新建一个pojo包,里面存放用到的实体类,路径为com.niko.pojo,这样src目录下会自动多出两个文件夹,一个就是之前的包文件的文件夹jdbc,另一个是新建的包pojo

    image-20240709164709066

    image-20240709164024548

  • 接着编写Account.java,快捷键Alt+Insert选中变量自动添加gettersetter方法如下

    package com.niko.pojo;
    
    public class Account {
        private int houseid;
        private String address;
        private double price;
    
        public int getHouseid() {
            return houseid;
        }
    
        public void setHouseid(int houseid) {
            this.houseid = houseid;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    }
    
    

    继续添加toString方法便于查看,还是使用相同的快捷键添加。

    image-20240709164521862

  • 接下来完成JDBC代码的编写,新建一个类叫做JDBCExample1_ResultSet_HouseList,与pojo包里的文件名字对应(养成好习惯)。

    在这里插入图片描述

    接着明确我们的实现思路:定义实体类->从数据库查询数据(JDBC)->查询出来的数据封装到HouseList对象中->输出对象结果 或 进行网页显示等进一步的需求

    我们的实体类HouseList已经定义完成,接着我们在程序里要做的包括实体类实例化、数据库数据赋值给实体类HouseList、将实体类实例存入集合。

    关键代码如下:

    public class JDBCExample1_ResultSet_HouseList {
    
        /*
        * 查询houselist房屋表数据,封装为HouseList对象中,并且存储到ArrayList集合中
        * 1. 定义实体类HouseList
        * 2. 查询数据,封装到HouseList对象中
        * */
        @Test
        public void test() throws Exception {
            //1.注册驱动
            Class.forName("com.mysql.jdbc.Driver");
    
            //2.获取连接
            String url="jdbc:mysql://localhost:3306/db?useSSL=false";
            String username="root";
            String password="1234";
            Connection conn = DriverManager.getConnection(url, username, password);
    
            //3.定义sql
            String sql = "SELECT * FROM houselist";
    
            //4.获取执行sql的对象 Statement
            Statement stmt = conn.createStatement();
    
            //5.执行sql
            //int count =stmt.executeUpdate(sql); //返回值是受影响的行数
            ResultSet rs = stmt.executeQuery(sql);
    
            //创建集合(用到了泛型和多态)
            List<HouseList> list = new ArrayList<HouseList>();
    
            //6.处理结果
                //光标向下移动一行,并且判断当前行是否有数据
            while (rs.next()) {
                HouseList houseList=new HouseList();
                int id = rs.getInt("houseid");
                String address = rs.getString("address");
                double price = rs.getDouble("price");
    
                //赋值
                houseList.setHouseid(id);
                houseList.setAddress(address);
                houseList.setPrice(price);
    
                //存入集合
                list.add(houseList);
            }
    
            //查看数据是否输出成功了
            System.out.println(list);
    
            //7.释放资源
            rs.close();
            stmt.close();
            conn.close();
        }
        
    }
    

    一个对象(也就是表里的一行)的输出结果如下:

    image-20240709171943084

    要是没有在实体类里重载toString方法,会出现这样的输出结果:

    image-20240709172314139

    查资料可知,我们所看到的输出是Java中对象的默认toString()方法生成的结果。默认情况下,Java的toString()方法返回类名的完全限定名称(即包名+类名)后跟对象的哈希码的十六进制表示。

    具体来说,com.niko.pojo.HouseList@1c2c22f3表示:

    • com.niko.pojo.HouseList是对象的类名。
    • 1c2c22f3是对象的哈希码的十六进制表示。
5.PreparedStatement

PreparedStatement就是继承自Statement 的执行SQL语句的对象,作用就是预编译SQL语句并执行来预防SQL注入问题

1)SQL注入问题

摘自维基百科SQL注入 - 维基百科,自由的百科全书 (wikipedia.org)

SQL注入(英语:SQL injection),也称SQL注入SQL注码,是发生于应用程序与数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了字符检查,那么这些注入进去的恶意指令就会被数据库服务器误认为是正常的SQL指令而执行,因此遭到破坏或是入侵。

最简单的例子就是输入用户名和密码,这个过程中肯定会用到数据库的查询操作,比如说:

SELECT * FROM users WHERE username = 'username' AND password = 'password';

如果攻击者输入

admin' -- 

则服务器端接收到后可能会形成如下错误的SQL查询:

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

注释符--后面的文本将被视为注释,因此该查询实际上变成了:

SELECT * FROM users WHERE username = 'admin';

这样攻击者无需正确的密码即可成功登录。

因此PreparedStatement就相当于一道门卫岗,需要拦住特殊的非法查询,能增强数据库的安全性。(现在知道了为什么用户名或密码会限制特殊符号了)

idea快捷输入sout,回车后会自动补全为System.out.println()

package com.niko.jdbc;

import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
/*
 *  JDBC API 详解 PreparedStatement
 *  以用户登录为例
 *  附上SQL语句:
 	CREATE TABLE USER(
        id int,
        username VARCHAR(20),
        password VARCHAR(32)
	);
    INSERT INTO USER VALUES(1,'NIKO','123'),(2,'MONESY','456');
 * */

public class JDBCDemo5_PreparedStatement {

    /*
    * 正常登录
    * */
    @Test
    public void testDQL() throws Exception {



        //获取连接
        String url="jdbc:mysql://localhost:3306/db2?useSSL=false";
        String username="root";
        String password="1234";
        Connection conn = DriverManager.getConnection(url, username, password);

        //接收用户输入 用户名和密码
        String name="NIKO";
        String pwd="123";

        String sql="select * from user where username='"+name+"'and password='"+pwd+"'";

        //获取stmt对象
        Statement stmt=conn.createStatement();

        //执行sql
        ResultSet rs=stmt.executeQuery(sql);

        //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
        if(rs.next()) {
            System.out.println("SUCCESS");
        }else {
            System.out.println("FAIL");
        }

        //释放资源
        rs.close();
        stmt.close();
        conn.close();
    }

    /*
     * SQL注入
     * */
    @Test
    public void testDQL_Injection() throws Exception {



        //获取连接
        String url="jdbc:mysql://localhost:3306/db2?useSSL=false";
        String username="root";
        String password="1234";
        Connection conn = DriverManager.getConnection(url, username, password);

        //接收用户输入 用户名和密码
        String name="NIKO";//随便写,不用管数据库里有没有
        String pwd="' OR '1'='1";

        String sql="select * from user where username='"+name+"'and password='"+pwd+"'";
        System.out.println(sql);

        //获取stmt对象
        Statement stmt=conn.createStatement();

        //执行sql
        ResultSet rs=stmt.executeQuery(sql);

        //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
        if(rs.next()) {
            System.out.println("SUCCESS 注入成功!");
        }else {
            System.out.println("FAIL");
        }

        //释放资源
        rs.close();
        stmt.close();
        conn.close();
    }

}

2)使用PreparedStatement解决问题

使用方法:

  • 获取 PreparedStatement 对象

    //初始:sql注入本质就是在拼sql语句字符串
    String sql="select * from user where username='"+name+"'and password='"+pwd+"'";
    
    //解决:SQL语句中的参数值现在使用占位符代替
    String sql="select * from user where username=? and password=?";
    //调用conn对象的方法,将这个sql语句作为参数传入Pstmt对象
    PreparedStatement pstmt=conn.prepareStatement(sql);
    
  • 设置参数值

    ​ 参数按照顺序 一 一对应各个?号。

    image-20240709182235015

    ​ 调用pstmt的方法setXxx(),在此示例中用户名和密码两个都是字符串类型。

    //设置?号的值
    pstmt.setString(1,name);
    pstmt.setString(2,pwd);
    
  • 执行SQL并处理结果

    ​ 要注意这里不能调用父类的带参数的executeQuery方法,因为预编译的特性,SQL语句已经和pstmt对象绑定,所以调用的是空参的同名方法。

    	    //执行sql
            ResultSet rs=pstmt.executeQuery();
    
            //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
            if(rs.next()) {
                System.out.println("SUCCESS");
            }else {
                System.out.println("FAIL");
            }
    
            //释放资源
            rs.close();
            pstmt.close();
            conn.close();
        }
    

    ​ 很显然,再次传入一样的账户名和密码进行SQL注入测试,输出FAIL表示登录失败,登录被拒绝。

    image-20240710000814234
    类型。

    //设置?号的值
    pstmt.setString(1,name);
    pstmt.setString(2,pwd);
    
  • 执行SQL并处理结果

    ​ 要注意这里不能调用父类的带参数的executeQuery方法,因为预编译的特性,SQL语句已经和pstmt对象绑定,所以调用的是空参的同名方法。

    	    //执行sql
            ResultSet rs=pstmt.executeQuery();
    
            //判断登录是否成功(rs.next()的作用是判断当前行有无数据并下移一行)
            if(rs.next()) {
                System.out.println("SUCCESS");
            }else {
                System.out.println("FAIL");
            }
    
            //释放资源
            rs.close();
            pstmt.close();
            conn.close();
        }
    

    ​ 很显然,再次传入一样的账户名和密码进行SQL注入测试,输出FAIL表示登录失败,登录被拒绝。

在这里插入图片描述

3) PreparedStatement原理

2024年7月10日09:31:35更新

PreparedStatement有这样几个好处:

  • 预编译SQL,提高SQL语句执行性能

    ​ 我们已经学了Java代码是如何和MySQL数据库进行交互的:Java代码将待执行的SQL语句发给MySQL服务器,MySQL服务器在接收SQL语句并执行后返回结果给Java代码。

    ​ 其中MySQL服务器在接受SQL语句之后并不是直接执行SQL代码的,而是需要经过 检查SQL语法->编译SQL得到可执行的函数 -> 执行SQL 几个步骤。其中前两个步骤----检查和编译 过程相比 执行 需要耗费相对更多的时间。

    ​ 对于同样结构的SQL语句,比如

    SELECT * FROM USER WHERE USERNAME = ?
    setString(1,"Mike");
    setString(2,"Jane");
    

    ​ 只需执行一次检查与编译步骤,节省了时间,因此能够提高性能。

    ​ 但是值得注意的是PreparedStatement的预编译功能默认是关闭的,需要在数据库URL的后面加上参数useServerPrepStmts=true才能开启。

    String url="jdbc:mysql://localhost:3306/db2?useSSL=false"?useServerPrepStmts=true;
    
  • 解决SQL注入问题

    上一次我们学到Pstmt防止SQL注入的原理是 将敏感字符进行转义

    为了可视化这个过程,我们还需要配置MySQL执行日志(重启MySQL服务器后生效),在MySQL安装过程中相信大家都碰到过my.ini这个配置文件,修改这个文件,加入以下内容:

    # 将日志输出设置为文件
    log-output=FILE
    
    # 启用一般查询日志
    general-log=1
    
    # 指定一般查询日志文件的位置和名称
    general_log_file="D:\\mysql.log"
    
    # 启用慢查询日志
    slow-query-log=1
    
    # 指定慢查询日志文件的位置和名称
    slow_query_log_file="D:\\mysql_slow.log"
    
    # 设置慢查询的阈值时间为2秒(查询时间超过2秒的语句将被记录到慢查询日志中)
    long_query_time=2
    
    

    找不到这个文件的可以找找隐藏目录C:\ProgramData\MySQL\MySQL Server 8.0\my.ini(Windows)

    屏幕截图 2024-07-10 010300

    关于如何查找MySQL安装目录,可参考如何查看MySql的安装位置?_mysql安装路径怎么找-CSDN博客

    接着重启MySQL服务,这里使用的是以管理员身份运行的cmd,当然使用services.msc命令进入图形界面修改也可以。

    C:\Windows\System32>net stop mysql
    MySQL 服务正在停止.
    MySQL 服务已成功停止。
    
    C:\Windows\System32>net start mysql
    MySQL 服务正在启动 .
    MySQL 服务已经启动成功。
    
    

    ​ 查看log文件来了解是否开启了预编译,可以看到开启预编译之后确实是通过对敏感字符转义字符来实现。还可以测试一下同一结构的两个语句,可以发现只有一次prepare提示,两次execute提示,说明只有一次检查和编译过程,但是有两次执行过程。

    开启预编译之后:

    image-20240710013106378

    开启预编译之前:

    image-20240710013225927

学习过程参考05-JDBC-API详解-ResultSet_哔哩哔哩_bilibili黑马程序员

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值