1.Java访问数据库
JDBC 即Java Database Connectivity的简称
- 在Java中用于规范如何访问关系型数据库(提供访问不同数据库的接口) 由各大数据库进行具体的实现
- 这样好处在于我们在切换数据库的时候 不用改写我们访问数据库的代码 因为切换数据库的过程中 JDBC保持不动 他所提供的数据库访问的API也没改变 切换的是底层的具体实现 我们调用的时候 只需要关心JDBC 而非不同数据库的具体实现
- 属于JavaSE的一部分
- 这说明了在普通的Java项目中 也能够访问数据库 并不局限于web项目中
- JavaEE包含了JavaSE JDK实现了JavaSE(JDK帮助JavaSE项目编译、运行 并且提供了JavaSE所需的API) JavaSE包含了JDBC(由数据库厂商推出的具体实现则内置于JDK中)
2.下载MySQL的JDBC实现(jar/驱动包)
- 8.0版本的JDBC支持5.7、8.0版本的MySQL
- 我们可以去官网地址下载
- 下载完毕以后 我们可以将其添加到JavaSE项目中的lib文件夹 也可以将其添加到JavaEE项目中web-inf中的lib文件夹 并且我们要通过add as library将JDBC的jar包添加到classpath 选择module library即可
3.JDBC的使用步骤
JDBC添加到JavaSE以后 接下来关心的就是使用了
- 注册Driver(驱动程序)到DriverManager(驱动程序管理者)
- 目的在于将JDBC和数据库厂商提供的具体实现相绑定 在调用JDBC的API时才知道调用谁的具体实现
- 有两种驱动注册的方法:
- 对比一下的两种方法 显然方法二更有优势 他的好处在于:程序运行过程中只会创建一次Driver实例(底层的具体实现为静态初始化块中内置了注册方法 即DriverManager.registerDriver() 而静态初始化块在程序运行过程中只会执行一次) 而方法一则是会徒增堆内存的消耗 造成资源的浪费(方法一会造成明显的重复注册问题 而且在此过程中 并没有调用deregisterDriver方法用以释放申请的Driver堆内存 所以此过程只会造成堆内存的大量积压 甚至浪费)
- 方法一对于外部环境的改变(数据库的切换)不够灵活 切换数据库要求部分代码也要随之切换(不同的数据库 Driver类的导包代码不尽相同)
import com.mysql.cj.jdbc.Driver; import java.sql.DriverManager; public class Main { public static void main(String[] args) throws Exception { // 方法一 DriverManager.registerDriver(new Driver()); // 方法二 Class.forName("com.mysql.cj.jdbc.Driver"); } }
- 利用DriverManager创建Connection()数据库连接
- 使用数据库之前先要连接数据库
- url参数 不同的数据库不尽相同 url格式为协议://IP:端口号/数据库(协议控制访问的方式 IP定位服务器 端口号定位服务器软件 数据库定位数据库服务器软件上的数据库) mysql的协议为jdbc:mysql
import com.mysql.cj.jdbc.Driver; import java.sql.Connection; import java.sql.DriverManager; public class Main { public static void main(String[] args) throws Exception { // 将驱动程序注册到驱动程序管理者中 Class.forName("com.mysql.cj.jdbc.Driver"); // 利用DriverManager创建数据库连接 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh"); } }
- 利用Connection创建Statement(语句)
import com.mysql.cj.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws Exception {
// 将驱动程序注册到驱动程序管理者中
Class.forName("com.mysql.cj.jdbc.Driver");
// 利用DriverManager创建数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh");
// 利用Connection创建语句
Statement statement = connection.createStatement();
}
}
- 利用Statement执行sql语句
import com.mysql.cj.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws Exception {
// 将驱动程序注册到驱动程序管理者中
Class.forName("com.mysql.cj.jdbc.Driver");
// 利用DriverManager创建数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh");
// 利用Connection创建语句
Statement statement = connection.createStatement();
// 利用Statement执行sql语句 比如 我们想要修改customer表中id为1的年龄为100
statement.execute("UPDATE customer SET age = 100 WHERE id = 1");
}
}
- 关闭资源(关闭Connection、Statement)
- 先关闭Statement 在关闭Connection 保证资源的正常关闭
import com.mysql.cj.jdbc.Driver; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; public class Main { public static void main(String[] args) throws Exception { // 将驱动程序注册到驱动程序管理者中 Class.forName("com.mysql.cj.jdbc.Driver"); // 利用DriverManager创建数据库连接 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh"); // 利用Connection创建语句 Statement statement = connection.createStatement(); // 利用Statement执行sql语句 比如 我们想要修改customer表中id为1的年龄为100 statement.execute("UPDATE customer SET age = 100 WHERE id = 1"); // 关闭资源 statement.close(); connection.close(); } }
- 注意点:在连接数据库的时候 除了指定url以外 我们还需要指定数据库登录时的用户名和密码 并且可以在url之后拼接serverTimezone属性(属性值设置为UTC)解决服务器和MySQL JDBC驱动程序时区不一致的问题
import com.mysql.cj.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws Exception {
// 将驱动程序注册到驱动程序管理者中
Class.forName("com.mysql.cj.jdbc.Driver");
// 利用DriverManager创建数据库连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh?serverTimezone=UTC", "root", "root");
// 利用Connection创建语句
Statement statement = connection.createStatement();
// 利用Statement执行sql语句 比如 我们想要修改customer表中id为1的年龄为100
statement.execute("UPDATE customer SET age = 100 WHERE id = 1");
// 关闭资源
statement.close();
connection.close();
}
}
- 代码改进:我们可以将因数据库改动的值抽取出来用变量保存 方便修改 这些值包括:driverClassName、url、username、password
import com.mysql.cj.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws Exception {
// 抽取变量
String driverClassName = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/axihh?serverTimezone=UTC";
String username = "root";
String password = "root";
// 将驱动程序注册到驱动程序管理者中
Class.forName(driverClassName);
// 利用DriverManager创建数据库连接
Connection connection = DriverManager.getConnection(url, username, password);
// 利用Connection创建语句
Statement statement = connection.createStatement();
// 利用Statement执行sql语句 比如 我们想要修改customer表中id为1的年龄为100
statement.execute("UPDATE customer SET age = 100 WHERE id = 1");
// 关闭资源
statement.close();
connection.close();
}
}
- 异常处理:利用try-catch配合try-with-resources(可以自动关闭资源Connnection、Statement)处理JDBC操作过程中抛出的异常
import com.mysql.cj.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws Exception {
// 抽取变量
String driverClassName = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/axihh?serverTimezone=UTC";
String username = "root";
String password = "root";
try {
// 将驱动程序注册到驱动程序管理者中
Class.forName(driverClassName);
try(Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
) {
// 利用Statement执行sql语句 比如 我们想要修改customer表中id为1的年龄为100
statement.execute("UPDATE customer SET age = 100 WHERE id = 1");
}
}catch(Exception e){
e.printStackTrace();
}
}
}
4.JDBC版本
- JDBC版本
- JDBC 4.0包含在Java SE 6.0中
- JDBC 4.1包含在Java SE 7.0中
- JDBC 4.2包含在Java SE 8.0中
- JDBC 4.3包含在Java SE 9.0中
- 从JDBC 4.0开始 显式注册驱动是可选的 我们只需要将厂商提供的Jar包添加到类路径中 DriverManager就可以自动检测并加载驱动程序
5.JDBC细节
- 不同版本的MySQL驱动包存在一些差异
- 驱动类名方面 MySQL驱动包6.x以前为com.mysql.jdbc.Driver 而MySQL驱动包6.x及以后则为com.mysql.cj.jdbc.Driver
- 时区方面 MySQL驱动包6.x以前无需设置 而6.x及以后则需要在连接数据库时url之后拼接确定的时区 即serverTimezone=UTC
- MySQL驱动包移除类路径
- 在project structure中找到MySQL驱动包作用的子模块
- 选中 找到他的依赖 查找依赖列表 移除所依赖的MySQL驱动包
- 由于子模块不在依赖于MySQL驱动包 这就意味着模块不知道他的存在 也就不能完成对其API的调用了
- 虽然在新版本的MySQL驱动包中 旧的驱动类名被废弃 但是依然可以正常使用
- 在新版本的驱动包中 旧版本的类名继承了新版本的类名 从Jar包中的字节码可以了解到旧类名继承自新类名 可想而知 在JVM装载字节码的过程中 先加载父类在加载子类 即先执行父类的静态初始化块在执行子类的
- Class.forName对应的就是JVM装载字节码的操作
- 子模块不允许同时依赖于两个及以上的Jar包
- 一旦允许 那么有可能Jar包的API有同名的风险 在调用的时候 就会产生歧义
6.Statement的常用API
- ResultSet executeQuery(String sql)
- 执行DQL语句
- ResultSet的常用API
- boolean next()
- 让游标指向下一行 如果指向的当前行有数据的话 那么返回true 否则返回false(最开始游标指向字段名一行)
- xx getxx(int columnIndex)、xx getxx(String columnLabel)
- 通过字段索引(从1开始计数)/字段名获取当前行某一列的数据 建议通过字段名获取(表格结构一旦发生改变 索引可能随之变化 但是字段名不轻易变化)
- 通过字段名获取的过程中 字段名不一定是数据库中真正的字段名 有可能是别名
- boolean next()
- int executeUpdate(String sql)
- 执行DML、DDL语句
- 如果是DML语句 返回值表示受到影响的记录数量 如果没有返回值的话 通常就返回0
- 对比execute和executeUpdate 显然后者对于执行DML语句更合适 因为他的返回值更有意义
7.Statement的sql注入问题
sql注入:就是插入恶意的sql语句达到绕过身份验证等目的 具体体现在登录时可以随便输入用户名、密码
- 以下是一个典型的sql注入案例 其中 他嵌入了恶意的
' OR '1' = '1
代码 导致sql语句变成了SELECT * FROM user WHERE username = '11111' AND password = '11111' OR '1' = '1'
其中 由于and的优先级高于or 所以where子句相当于SELECT * FROM user WHERE (username = '11111' AND password = '11111') OR '1' = '1'
所以无论你的用户名和密码如何填写 结果都是登录成功的
import java.sql.*;
/*
抛异常的处理方式我们用try-catch替代throws的做法 并且通过try-with-resources来自动关闭资源
有一些地方是随着数据库的改变而改变的 我们可以将他们抽取出来用变量保存 这样就可以集成在一起设置了
从JDBC 4.0开始 驱动程序可以无需显式注册 可以由驱动程序管理者自动加载了 即第一步可以省略 只要你的JDK版本在6.0以上 那么对应的Java SE都会包含JDBC 4.0及以上的版本
*/
public class Main {
private static void login(String username, String password) throws Exception {
// 连接数据库
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/axihh?setTimezone=UTC", "root", "root");
// 创建语句
Statement stmt = conn.createStatement();
// 执行sql语句
ResultSet rs = stmt.executeQuery("SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'");
System.out.println("SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'");// SELECT * FROM user WHERE username = '111111' AND password = '1111111' OR '1' = '1'
// 执行的结果要么是1条 要么是0条
if(rs.next()){
System.out.println("登陆成功");// 登陆成功
}else{
System.out.println("登陆失败 用户名或密码错误");
}
}
public static void main(String[] args) throws Exception {
String username = "111111";
String password = "1111111' OR '1' = '1";
login(username, password);
}
}
8.PreparedStatement
- PreparedStatement接口继承自Statement接口 在继承了Statement的一系列规范之后又增加了自己的个性化接口 属于在Statement的基础上强化了功能
- 建议PreparedStatement代替Statement
-
PreparedStatement的好处
- 可以防止sql注入(PreparedStatement会将用户名和密码原原本本带入检验)
参数插入sql语句之后 由于插入的参数为字符串 所以相当于自动添加’'/""来包裹字符串 我们只需要关心内容的部分
''包裹:SEELCT * FROM user WHERE username = ‘111111’ AND password = ‘111111’ OR ‘1’ = ‘1’;(注意 为了保持密码外包裹的单引号生效 所以会对传入的字符串中的单引号进行转义操作)
""包裹:SELECT * FROM user WHERE username = “111111” AND password = “111111 OR ‘1’ = '1”;
这种形式都会将传入的用户名和密码原原本本的带入到数据库中进行检验 而不会像Statement一样被恶意拼串导致sql注入问题的产生import java.sql.*; /* 抛异常的处理方式我们用try-catch替代throws的做法 并且通过try-with-resources来自动关闭资源 有一些地方是随着数据库的改变而改变的 我们可以将他们抽取出来用变量保存 这样就可以集成在一起设置了 从JDBC 4.0开始 驱动程序可以无需显式注册 可以由驱动程序管理者自动加载了 即第一步可以省略 只要你的JDK版本在6.0以上 那么对应的Java SE都会包含JDBC 4.0及以上的版本 */ public class Main { private static String URL = "jdbc:mysql://localhost:3306/axihh?setTimezone=UTC"; private static String USERNAME = "root"; private static String PASSWORD = "root"; private static void login1(String username, String password) throws Exception { // 连接数据库 Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); // 创建语句 Statement stmt = conn.createStatement(); // 执行sql语句 ResultSet rs = stmt.executeQuery("SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'"); System.out.println("SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'");// SELECT * FROM user WHERE username = '111111' AND password = '1111111' OR '1' = '1' // 执行的结果要么是1条 要么是0条 if(rs.next()){ System.out.println("登陆成功");// 登陆成功 }else{ System.out.println("登陆失败 用户名或密码错误"); } } private static void login2(String username, String password) throws Exception { // 连接数据库 Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); // 创建语句 同时传递sql语句 PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM user WHERE username = ? AND password = ?"); // 设置参数值 pstmt.setString(1, username); pstmt.setString(2, password); // 执行sql语句 ResultSet rs = pstmt.executeQuery(); // 判断登录成功与否 if(rs.next()){ System.out.println("登陆成功"); }else { System.out.println("登录失败 用户名或者密码错误");// 登录失败 用户名或者密码错误 } } public static void main(String[] args) throws Exception { String username = "111111"; String password = "1111111' OR '1' = '1"; login1(username, password); login2(username, password); } }
- 在执行速度上PreparedStatement比Statement快
- 每一次的传入的sql参数都必须经历三个步骤:1.编译 2.解析 3.优化 而PreparedStatement是先创建语句(同时传入sql参数)后执行 Statement则是先创建语句后执行(同时传入参数) 很明显 一旦我们执行的sql语句多了以后 那么前者所需经历的步骤肯定要比后者少 自然在效率上前者要比后者高
-