JDBC你总得学着去连接数据库

50次阅读

共计 10331 个字符,预计需要花费 26 分钟才能阅读完成。

BWH_Steven:带 JDBC 再爱你一次

(一) JDBC 入门

(1) 概述

Java 数据库连接,(Java Database Connectivity,简称 JDBC)是 Java 语言中用来规范 客户端 程序如何来 访问数据库 的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC 也是 Sun Microsystems 的商标。JDBC 是 面向关系型 数据库的。

简单解释: 通过 Java 语言执行 sql 语句,从而操作数据库

(2) 来由

想要通过 Java 操作不同的数据库,应该根据数据库的不同而执行特定的 API,而出于简化的想法,Sun 公司,定义了一套面向所有关系型数据库的 API 即 JDBC,其只提供接口,而具体实现去交给数据库厂商实现,而我们作为开发者,我们针对数据数据库的操作,只需要基于 JDBC 即可

(二) 简单使用 JDBC

我们简单的使用 JDBC 去查询数据库中的数据,并且输出到控制台中

为了快速演示,我们新建一张非常简单的表

CREATE TABLE student(
    id INT PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(20),
    score DOUBLE(4,1)
);

INSERT student(id,NAME,score) VALUES (1,'张三',98);

INSERT student(id,NAME,score) VALUES (2,'李四',96);

INSERT student(id,NAME,score) VALUES (3,'王五',100);

我们根据数据库中的信息写一个对应的学生类

public class Student {
    private int id;
    private String name;
    private double score;
    // 省略构造、Get、Set、toString 方法
    ...... 
}

下面是对 JDBC 查询功能的简单使用

package cn.ideal.jdbc;

import cn.ideal.domain.Student;

import java.sql.*;

public class JdbcDemo {public static void main(String[] args) {
        // 导入数据库驱动包

        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            // 加载驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 获取与数据库的连接对象
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1", "root", "root99");
            // 定义 sql 语句
            String sql = "SELECT * FROM student";
            // 获取执行 sql 语句的对象 statement
            statement = connection.createStatement();
            // 执行 sql 语句,获取结果集
            resultSet = statement.executeQuery(sql);

            // 遍历获取到的结果集
            while (resultSet.next()) {int id = resultSet.getInt(1);
                String name = resultSet.getString(2);
                Double score = resultSet.getDouble(3);

                Student student = new Student();
                student.setId(id);
                student.setName(name);
                student.setScore(score);

                System.out.println(student.toString());
            }

        } catch (ClassNotFoundException e) {e.printStackTrace();
        } catch (SQLException e) {e.printStackTrace();
        } finally {
            // 释放资源,后调用的先释放
            if (resultSet != null) {
                try {resultSet.close();
                } catch (SQLException e) {e.printStackTrace();
                }
            }

            if (statement != null) {
                try {statement.close();
                } catch (SQLException e) {e.printStackTrace();
                }
            }

            if (connection != null) {
                try {connection.close();
                } catch (SQLException e) {e.printStackTrace();
                }
            }
        }
    }
}

// 运行结果
Student{id=1, name='张三', score=98.0}
Student{id=2, name='李四', score=96.0}
Student{id=3, name='王五', score=100.0}

下面我们开始详细的解释一下上面所用到的各个对象

(三) JDBC 对象详解

(1) DriverManager

A:加载驱动 –> 注册驱动

首先我们要知道加载驱动和注册驱动这两个词是什么意思,刚刚接触的时候,会有人总有朋友将 Class.forName(com.mysql.jdbc.Driver) 当做注册数据库驱动的语句,但实际不然,它的作用是将参数表示的类加载到内存中,并且 初始化,同时其中的静态变量也会被初始化,静态代码块也会被执行

  • 疑惑:能否使用 ClassLoader 类中的 loadClass()方法呢?

    • 答案是否定的,这个方法的特点是加载但不对该类初始化
//Class 类源码节选 -jdk8
* A call to {@code forName("X")} causes the class named
* {@code X} to be initialized.

关于初始化问题这里简单提及一下,我们还是先回到我们主线来

为什么不对类进行初始化,就不能选择了呢?

这是因为真正实现 注册驱动(告诉程序使用哪一个数据库驱动 jar)的是:

static void registerDriver(Driver driver)

我们在 jar 包中找到 Driver 这个类,查看其源码

//com.mysql.jdbc.Driver 类中的静态代码块
static {
    try {DriverManager.registerDriver(new Driver());
    } catch (SQLException var1) {throw new RuntimeException("Can't register driver!");
    }
}

类被加载后,执行了类中的静态方法 DriverManager 进行了注册驱动

我们也可能有见过下面 2 中的代码,但是实际上驱动会被加载两次,因为执行

new com.mysql.jdbc.Driver() 已经加载了一次驱动

//1. 推荐
Class.forName("com.mysql.jdbc.Driver");
//2. 不推荐
DriverManager.registerDriver(new com.mysql.jdbc.Driver())

那么何必这么麻烦呢?new com.mysql.jdbc.Driver() 直接这样写不就挺好了吗?

但我们还是选择 拒绝!为什么呢?

如果我们这样写,对于 jar 包的依赖就比较重了,我们如果面临多个项目,或者需要修改数据库,就需要修改代码,重新编译,但是如果使用 Class 类加载的方式,既保证了静态代码块中所包含的注册驱动方法会被执行,而又将参数变成了字符串形式,我们之后便可以通过修改配置文件“”内的内容 + 添加 jar 包 的方式更灵活的处理问题,并且不需要重新编译!

注意:mysql5 之后的驱动 jar 包可以省略注册驱动这一步,原因查看 jar 包中 META-INF/services/java.sql.Driver 文件

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

B:获取数据库连接

static Connection getConnection(String url, String user, String password) 
/*
    jdbc:mysql://ip 地址(域名): 端口号 / 数据库名称
    Eg:jdbc:mysql://localhost:3306/db1
    本地 mysql,且端口为默认 3306,则可简写:jdbc:mysql:/// 数据库名称
*/

(2) Connection (数据库连接对象)

A:获取执行 sql 的对象

// 创建向数据库发送 sql 语句的 statement 对象
Statement createStatement()

// 创建向数据库发送预编译 sql 语句的 PrepareStement 对象
PreparedStatement prepareStatement(String sql)  

B:管理事务

// 开启事务:设置参数为 false,即开启事务
setAutoCommit(boolean autoCommit) 

// 提交事务
commit() 

// 回滚事务
rollback() 

(3) Statement (执行 sql 语句的对象)

// 执行 DQL(查询数据库中表的记录(数据))ResultSet executeQuery(String sql)

// 执行 DML(对数据库中表的数据进行增删改)int executeUpdate(String sql)

// 执行任意 sql 语句,但是目标不够明确,较少使用
boolean execute(String sql)

// 把多条 sql 的语句放到同一个批处理中
addBatch(String sql)

// 向数据库总发送一批 sql 语句执行
executeBatch()

代码演示(以增加一条数据为例)

package cn.ideal.jdbc;

import java.sql.*;

public class StatementDemo {public static void main(String[] args) {

        Connection connection = null;
        Statement statement = null;
        try {
            // 加载驱动
            Class.forName("com.mysql.jdbc.Driver");

            // 获取数据库连接对象
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1", "root", "root99");

            // 定义 sql 语句
            String sql = "INSERT student(id,NAME,score) VALUES (NULL,' 马六 ',88);";

            // 获取执行 sql 语句的对象
            statement = connection.createStatement();

            // 执行 sql 语句
            int count = statement.executeUpdate(sql);
            System.out.println(count);
            if (count > 0) {System.out.println("添加成功");
            } else {System.out.println("添加失败");
            }
        } catch (ClassNotFoundException e) {e.printStackTrace();
        } catch (SQLException e) {e.printStackTrace();
        }finally {if(statement != null){
                try {statement.close();
                } catch (SQLException e) {e.printStackTrace();
                }
            }

            if (connection != null){
                try {connection.close();
                } catch (SQLException e) {e.printStackTrace();
                }
            }
        }
    }
}

(4) ResultSet(结果集对象,封装查询结果)

ResultSet 所代表的的是 sql 语句的结果集——执行结果,当 Statement 对象执行 excuteQuery()后,会返回一个 ResultSet 对象

// 游标向下移动一行,判断当前行是否是最后一行末尾(是否有数据)
// 如果是,则返回 false,如果不是则返回 true
boolean next()

// 获取数据,Xxx 代表数据类型  
getXxx(参数)

Eg:int getInt() ,    String getString()
    
1. int:代表列的编号, 从 1 开始   如:getString(1)
2. String:代表列名称。如:getDouble("name")

案例可参考开头快速使用部分,自行尝试读取数据库中数据后用集合框架装载

(四) 事半功倍——工具类

通过封装一些方法,使得出现一个更加通用的工具类,我们可以通过 properties 配置文件,使得信息更加直观且容易维护

package cn.ideal.jdbc;

import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.*;
import java.util.Properties;

public class JDBCUtils {
    private static String url;
    private static String user;
    private static String password;
    private static String driver;

    /**
     * 文件读取
     */
    static {

        try {
            // 创建 Properties 集合类
            Properties pro = new Properties();
            // 获取 src 路径下的文件
            ClassLoader classLoader = JDBCUtils.class.getClassLoader();
            URL res = classLoader.getResource("jdbc.properties");
            String path = res.getPath();

            // 加载文件
            pro.load(new FileReader(path));
            // 获取数据
            url = pro.getProperty("url");
            user = pro.getProperty("user");
            password = pro.getProperty("password");
            driver = pro.getProperty("driver");

            // 注册驱动
            Class.forName(driver);
        } catch (IOException e) {e.printStackTrace();
        } catch (ClassNotFoundException e) {e.printStackTrace();
        }

    }

    /**
     * 获取连接
     *
     * @return 连接对象
     */
    public static Connection getConnection() throws SQLException {return DriverManager.getConnection(url, user, password);
    }

    /**
     * 释放资源
     *
     * @param statement
     * @param connection
     */
    public static void close(Statement statement, Connection connection) {if (statement != null) {
            try {statement.close();
            } catch (SQLException e) {e.printStackTrace();
            }
        }

        if (connection != null) {
            try {connection.close();
            } catch (SQLException e) {e.printStackTrace();
            }
        }
    }

    /**
     * 释放资源
     *
     * @param resultSet
     * @param statement
     * @param connection
     */
    public static void close(ResultSet resultSet, Statement statement, Connection connection) {if (resultSet != null) {
            try {resultSet.close();
            } catch (SQLException e) {e.printStackTrace();
            }
        }

        if (statement != null) {
            try {statement.close();
            } catch (SQLException e) {e.printStackTrace();
            }
        }

        if (connection != null) {
            try {connection.close();
            } catch (SQLException e) {e.printStackTrace();
            }
        }
    }

}

工具类测试类

package cn.ideal.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class JDBCUtilsTest {public static void main(String[] args) {

        Connection connection = null;
        Statement statement = null;
        try {connection = JDBCUtils.getConnection();

            // 定义 sql 语句
            String sql = "INSERT student(id,NAME,score) VALUES (NULL,' 马六 ',88)";

            // 获取执行 sql 语句的对象
            statement = connection.createStatement();

            // 执行 sql 语句
            int count = statement.executeUpdate(sql);
            System.out.println(count);
            if (count > 0) {System.out.println("添加成功");
            } else {System.out.println("添加失败");
            }
        } catch (SQLException e) {e.printStackTrace();
        } finally {JDBCUtils.close(statement,connection);
        }
    }
}

之前的文章中分别通过集合实现、IO 实现、而学习数据库后,我们可以试着通过数据库存储数据,写一个简单的登录注册小案例!在第五大点中有提到吼

(五) 补充:PreparedStatment

// 创建向数据库发送预编译 sql 语句的 prepareStatement
PreparedStatement prepareStatement(String sql) 

prepareStatement 继承自 Statement,总而言之,它相较于其父类,更强更简单!

(1) 优点

A:效率

Statement 直接编译 SQL 语句,直接送到数据库去执行,而且其多次重复执行 sql 语句,PreparedStatement 会 对 SQL 进行预编译 ,再填充参数,这样效率会比较高( 预编译的 SQL 存储在 PreparedStatement 中

B:可读性

定义 SQL 语句的时候,常常需要使用到 Java 中的变量,在一些复杂的情况下,需要频繁的使用到引号和单引号的问题,变量越多,越复杂,而 PreparedStatement 可以使 用占位符‘?’代替参数,接下来再进行参数的赋值,这样有利于代码的可读性

C:安全性

PreparedStatement 由于预编译,可以避免 Statement 中可能需要采取字符串与变量的拼接而导致 SQL 注入攻击【编写永等式,绕过密码登录】

我们先按照我们之前的做法,写一个简单的登录 Demo,先创一张表!

CREATE TABLE USER(
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(32),
    PASSWORD VARCHAR(32)
);

SELECT * FROM USER;

INSERT INTO USER VALUES(NULL,'admin','admin888');
INSERT INTO USER VALUES(NULL,'zhangsan','123456');

接着编写代码

package cn.ideal.login;

import cn.ideal.jdbc.JDBCUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;

public class LoginDemo {public static void main(String[] args) {Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名");
        String username = sc.nextLine();
        System.out.println("请输入密码");
        String password = sc.nextLine();
        
        boolean flag = new LoginDemo().login(username, password);

        if (flag) {System.out.println("登录成功");
        } else {System.out.println("用户名或密码错误");
        }
    }

    /**
     * 登录方法
     */
    public boolean login(String username, String password) {if (username == null || password == null) {return false;}

        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {connection = JDBCUtils.getConnection();
            // 定义 sql
            String sql = "SELECT * FROM USER WHERE username ='" + username + "'AND password ='" + password + "' ";
            // 获取执行 sql 的对象
            statement = connection.createStatement();
            // 执行查询
            resultSet = statement.executeQuery(sql);

            return resultSet.next();} catch (SQLException e) {e.printStackTrace();
        }finally {JDBCUtils.close(resultSet,statement, connection);
        }
        return false;
    }
}

简单的来说,这样一个简单的登录 Demo 就写好了,但是这个时候,SQL 注入问题中的一种情况就出现了,或许你听过,在早些年的时候,漏洞还是蛮常见的,一些黑客或者脚本小子们常常使用一些 SQL 注入的手段进行目标网站后台的入侵,我们今天所讲的这一种,就是其中一种,叫做 SQL 万能注入(SQL 万能密码)

我们先来观察一下上述代码中关于 SQL 语句的部分

 String sql = "SELECT * FROM USER WHERE username ='" + username + "'AND password ='" + password + "' ";

也就是说它将我们所输入的 usernamepassword合成为 SQL 查询语句,当数据库中不存在这样的字段就代表输入错误,但是对于存在 SQL 注入漏洞的程序,则可以通过构造一些特殊的字符串,达到登录的目的,先贴出来测试结果

// 运行结果
请输入用户名
admin
请输入密码
1'or'1'='1
登录成功

如果我们将上述代码中密码 (username) 部分用我们的这些内容代替是怎么样的呢

 String sql = "SELECT * FROM USER WHERE username ='admin'AND PASSWORD ='1'or'1'='1' ";

补充:在 SQL 语句中逻辑运算符具有优先级,= 优先于 and,and 优先于 or

所以上面的式子中 AND 先被执行,当然返回错,接着执行 or 部分,对于一个永等式‘1’=‘1‘来说返回值永远是 true,所以 SQL 查询结果为 true,即可以登录成功

// 使用 PrepareStemen 替代主要部分

// 定义 sql
String sql = "SELECT * FROM USER WHERE username = ? AND password = ?";
// 获取执行 sql 的对象
preparedStatement = connection.prepareStatement(sql);
// 给? 赋值
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);

// 执行查询
resultSet = preparedStatement.executeQuery();

// 运行结果
请输入用户名
admin
请输入密码
1'or'1'='1
用户名或密码错误

结尾:

如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家!^_^

如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创 Java 技术的公众号:理想二旬不止

正文完
 0