1.概述
我们学习了数据库,数据库实现了数据的持久化,但我们最终要在程序里处理数据啊,那java代码中怎么去访问数据库读写数据呢?
这就要用到sun公司设定的一套数据库标准了,这套标准就是JDBC(Java Database Connectivity)。但它只是规范,不做具体实现。于是数据库厂商又根据JDBC标准,实现自家的驱动Driver。如:mysql驱动com.mysql.cj.jdbc.Driver,Oracle的驱动oracle.jdbc.OracleDriver。有了这套解决方案,java就可以访问数据库中的数据了。
所以,JDBC是java database connectivity的简称,是专门用来完成 java程序 和 数据库 的连接的技术.
public interface Connection extends Wrapper, AutoCloseable {}
public interface Statement extends Wrapper, AutoCloseable {}
public interface PreparedStatement extends Statement {}
public interface CallableStatement extends PreparedStatement {}
public interface ResultSet extends Wrapper, AutoCloseable {}
Java中提倡面向接口开发,而最经典的接口设计莫过于JDBC数据库接口。
Connection链接、Statement语句、PreparedStatement预处理语句、CallableStatement存储过程、ResultSet结果集。
调用方式有三种:Statement语句、PreparedStatement预处理语句、CallableStatement存储过程,推荐使用第二种PreparedStatement,防止SQL注入,其也是预编译性能高。
2.JDBC使用前的准备工作
-
导入jar包(使用JDBC提供了丰富的工具类)
-
提供连接数据库的参数(用户名root 密码root 端口号3306)
-
在java程序中,发起SQL语句操作数据库
-
如果数据库有查到的结果,返回给java程序
2.1创建项目并导入jar包
- 创建project: File - New - Project - 选择java - next - next - 输入工程名称 - Finish
- 导入jar包:找到文件mysql-connector-java-5.1.32.jar 复制,粘贴到Project里
- 在IDEA里,选中jar包,右键编译(add as library…),ok
- 检查是否编译成功:看到IDEA里的jar包可以被点开了,就代表编译成功了
导入jar包后,我们便可以开始测试JDBC的连接了。
2.2 编写测试连接代码
我们创建一个TestConnection的类,用来测试JDBC连接;
JDBC是java连接数据库的一个标准,本质上就是一堆的工具类。要在JAVA代码中,使用JDBC连接,需要以下几步:
- 注册驱动
- 获取数据库连接
- 获取传输器
- 执行SQL,并返回结果集
- 处理数据库返回的结果
- 释放资源
2.2.1 注册驱动
首先,我们注册驱动,需要获取jar包的字节码对象,使用反射的技术原理,有三种方式可以获取注册驱动:
- Class.forName(“类的全路径”):使用全路径名
- 类名.class:直接使用类名
- 对象.getClass():直接使用对象名
因为我们是导入的jar包,并没有创建对象,并且其导入的目录在工程目录下,并不在我们创建类的同级目录下, 所以我们使用第一种放发,全路径名的方式获取其字节码对象。
Class.forName("com.mysql.cj.jdbc.Driver");
2.2.2 获取数据库连接
jdbc连接mysql数据库的协议需要使用DriverManager
类的getConnection
方法,其要求传入一个url链接,数据库名称与数据库的密码三个参数,其中url为数据库的协议地址,我们直接使用字符串定义其整个URL,然后将字符串作为参数传入,这样可以实现简单的解耦关系,之后如果使用的数据库有变化,直接更改url即可,并且对于代码的整齐程序也很多帮助;
下方是其方法的源代码:
这是我们最后完成的代码:
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
因为数据库在我们本地,所以我们使用localhost代表我们的服务器地址,后面跟着我们创建数据库时设置的端口号,然后是数据库的名称;
2.2.3 获取传输器
JDBC的传输器不止有一种,我们先使用createStatement
传输器,使用我们第二步创建的数据库连接对象来实现创建传输器,同时创建传输器的对象:
Statement s = c.createStatement();
2.2.4 执行SQL,并返回结果集
这里我们依旧使用解耦的思想编写代码:
首先编写SQL代码;
然后将SQL放入传输器对象的executeQuery
方法中,执行SQL;
传输器对象中共包含三种方法,分别是:
- executeQuery():用来执行查询的语句
- executeLargeUpdate():用来执行增删改
- ResultSet结果集:用来保存查询后的结果
因为我们这里测试能否正确连接数据库并查询其中的数据,所以我们使用executeQuery
方法:
String sql="select * from customers" ;
ResultSet r = s.executeQuery(sql);
2.2.5 处理数据库返回的结果
我们这里要对数据库返回的结果进行判断,所以要判断其返回结果中有没有值,有值,则一直循环获取,直接最后,所以这里使用while循环
进行判断;
在while循环
中,我们使用.next()
方法来确定获取的对象中有没有值;.next()
方法会查询下一条记录,有记录(有值)返回true并把记录内容存入到对应的对象中,也就是obj.next()的obj中。如果没有返回false。
while(r.next()){//next()判断resultset中有没有数据,有返回true
//getXxx()获取resultset中的数据
int a = r.getInt(1);//获取第1列的 整数值
String b = r.getString(2);//获取第2列的 字符串值
String c1 = r.getString(3);//获取第3列的 字符串值
System.out.println(a+" "+b+" "+c1);
}
在while
循环中,我们使用其列各自对应的数据类型方法接收对应的值,将其保存到对应的数据类型的变量中。
2.2.6 释放资源
最后,我们查询完成后,需要手动释放资源,包括结果集,传输器,连接器三个已经被占用的资源。
r.close();//释放结果集
s.close();//释放传输器
c.close();//释放连接器
使用其各自的对象,调用close()
方法释放资源即可;
2.3 完整代码展示
下面是我们测试的完整代码:
输入:
package cn.study;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class TestConnection {
public static void main(String[] args) throws Exception {
//1,注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");//全路径
//2,获取数据库的连接(用户名/密码)
//jdbc连接mysql数据库的协议//本机:端口号/数据库的名字
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
//3,获取传输器
Statement s = c.createStatement();
//4,执行SQL,并返回结果集
String sql="select * from customers" ;//查询dept表的所有数据
//executeQuery()用来执行查询的语句,executeLargeUpdate()用来执行增删改,ResultSet结果集,用来保存查询后的结果
ResultSet r = s.executeQuery(sql);
//5,处理数据库返回的结果
while(r.next()){//next()判断resultset中有没有数据,有返回true
//getXxx()获取resultset中的数据
int a = r.getInt(1);//获取第1列的 整数值
String b = r.getString(2);//获取第2列的 字符串值
String c1 = r.getString(3);//获取第3列的 字符串值
System.out.println(a+" "+b+" "+c1);
}
//6,释放资源
r.close();//释放结果集
s.close();//释放传输器
c.close();//释放连接器
}
}
输出:
3.模拟用户登录
3.1 准备测试数据
首先,我们在数据库中创建用户表,并插入一条用户数据:
CREATE TABLE tb_user(
id int PRIMARY KEY auto_increment,
name varchar(20) default NULL,
password varchar(20) default NULL
)
insert into tb_user values(null,'jack','321')
3.2 测试数据库连接,验证数据是否正确存在
首先,我们创建TestEntry
类,并创建method1( )
方法,用于测试数据库连接,查询上面创建的表即插入的表数据是否存在:
private static void method1() throws SQLException {
//1.调用工具类的方法
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
//2.获取传输器
Statement s = c.createStatement();
//3.执行SQL
ResultSet r = s.executeQuery("select * from tb_user");
//4.解析结果集
while(r.next()){//判断r有数据
//获取r的数据
int a = r.getInt("id");//获取表里的id字段的值
String b = r.getString("name");//获取表里的name字段的值
String c1 = r.getString("password");//获取表里的password字段的值
System.out.println(a+b+c1);
}
//5.释放资源
r.close();//释放结果集
s.close();//释放传输器
c.close();//释放连接器
}
3.3 测试用户登录
当我们使用jar包调用其工具类方法获取传输器时,需要每次都运用反射获取jar包中的方法,并传入数据库的名称及用户名、密码,这是十分繁琐的,所以我们将这一步封装到一个类中,这样我们今后只要直接调用它就可以了,而不用每次都手动完成,我们在创建JDBCUtils
类并在其中创建getConnection
用来封装这部分代码:
package cn.study;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCUtils {
public static Connection getConnection() throws Exception {
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");//全路径
//2.获取数据库连接
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
//3.返回给调用者
return c;
}
}
为了避免每次调用时都需要new一个新的对象调用,我们这里使用静态关键字描述方法,后续可以直接使用类名调用。
然后,我们在TestEntry
类中创建method2( )
方法,用于模拟用户登录;
用户登录时,会输入自己的账户名及密码,我们只要确定保存账户及密码的表中,有对应的数据可以与之匹配,那么则表明当前用户已经注册,并且用户名与密码都输入正确,可以登录;
private static void method2() throws Exception {
//1,获取传输器
Connection connection = JDBCUtils.getConnection();
Statement s = connection.createStatement();
//2,执行SQL
System.out.println("请输入用户名:");
String a = new Scanner(System.in).nextLine();//用户名
System.out.println("请输入密码:");
String b = new Scanner(System.in).nextLine();//密码
//如果动态的拼接字符串时,数据在中间的位置 "+a+"
// String sql="select * from tb_user where name='jack' and password='321'" ;
String sql="select * from tb_user where name='"+a+"' and password='"+b+"'" ;
ResultSet r = s.executeQuery(sql);
//3,解析结果集
if(r.next()){//查到数据了吗?查到了就登录成功
System.out.println("登录成功~");
}else{
System.out.println("用户名或者密码输入错误,登录失败~");
}
//4,关闭资源
r.close();
s.close();
connection.close();
}
在上面的代码中,我们还是先注册驱动,获取数据库连接并获取传输器;然后我们提示用户在控制台输入账号和密码并接收其具体的值;
接收到用户的值后,我们需要动态拼接值,将值放入SQL查询中作为WHERE子句传入;正常的WHERE子句中的值需要用单引号将其扩起来,表明这是一个字符串,动态拼接字符串时,也需要这样做,但是单引号中的值需要双引号及俩个加号包裹,这样才可以正确的动态拼接,你可以理解为,使用"+变量名+"
替换掉原本该放在WHERE子句位置的具体的值即可。
后续我们不再需要循环遍历获取的结果,我们只要判断是否正确返回了值即可,使用if语句
进行判断,如果正确返回值,那么则查询成功,代表可以成功登录,此时返回给客户成功登录的提示;如果没有查询到数据,那么代表数据库中不存在输入的账号及密码,此时返回给客户登录失败的提示即可;
在这里我们还要注意一个地方,在 2.2 小节我们编写测试连接代码时,连接数据库首先要通过反射原理注册驱动,Class.forName
可以指定class类路径进行动态创建对象实例,可JDBC这句话没有返回对象,这是为什么呢?
我们通过查看java.sql.Driver.class
的源码就找到真相了,原来它用了静态代码块创建对象。
但是这里我们并没有进行这一步同样连接成功并获取了数据,那么我们到底需要注册驱动吗?
其实这是因为java提供了SPI机制,用户可以自行配置类,JDBC高版本驱动就都引入了这个支持。如果用户使用了Class.forName方式就自己指定了驱动,如果未写这句话,则Java自动去META-INF/services/java.sql.Driver文件中找启动类。所以即使你不写,java也会在编译时自动帮你补全这段代码;
3.5 完整代码展示
到这里,我们测试用户登录就成功完成了,下面是测试的完整代码:
输入:
package cn.study;
import javax.sound.midi.Soundbank;
import java.sql.*;
import java.util.Scanner;
public class TestEntry {
public static void main(String[] args) throws Exception {
//method1();
method2();
}
private static void method2() throws Exception {
//1,注册驱动 2,获取连接
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
//3,获取传输器
Statement s = c.createStatement();
//4,执行SQL
System.out.println("请输入用户名:");
String a = new Scanner(System.in).nextLine();//用户名
System.out.println("请输入密码:");
String b = new Scanner(System.in).nextLine();//密码
//如果动态的拼接字符串时,数据在中间的位置 "+a+"
// String sql="select * from tb_user where name='jack' and password='321'" ;
String sql="select * from tb_user where name='"+a+"' and password='"+b+"'" ;
ResultSet r = s.executeQuery(sql);
//5,解析结果集
if(r.next()){//查到数据了吗?查到了就登录成功
System.out.println("登录成功~");
}else{
System.out.println("用户名或者密码输入错误,登录失败~");
}
//6,关闭资源
r.close();
s.close();
c.close();
}
private static void method1() throws SQLException {
//调用工具类的方法
String url="jdbc:mysql://localhost:3306/study2022" ;
Connection c = DriverManager.getConnection(url,"root","root");
//获取传输器
Statement s = c.createStatement();
//执行SQL
ResultSet r = s.executeQuery("select * from tb_user");
//解析结果集
while(r.next()){//判断r有数据
//获取r的数据
int a = r.getInt("id");//获取表里的id字段的值
String b = r.getString("name");//获取表里的name字段的值
String c1 = r.getString("password");//获取表里的password字段的值
System.out.println(a+b+c1);
}
//释放资源
r.close();//释放结果集
s.close();//释放传输器
c.close();//释放连接器
}
}
输出:
4. SQL注入问题及解决方案
我们创建TesTInjection
类,并复制TestEntry
类的method2( )
方法到TesTInjection
类中,并将方法名改为method1()
,然后我们运行代码:
我们在输入用户名时,输入jack'#
字符串,然后在提示输入密码时不输入密码,直接按回车,竟然登录成功了。首先数据库中并没有jack'#
这个用户,其次,我们也没有正确输入密码,为什么会登录成功呢?
其实这是因为SQL注入导致的攻击问题,本质上就是因为SQL语句中出现了特殊符号(#
,注释掉了一些条件),导致了SQL语义改变了。这里出现的#
让程序错误的以为,此次SQL语句执行到select * from tb_user where name='"+a+"'
的位置就已经结束了,SQL语句后面的部分,被认为是注释的信息(#
在sql语句中是注释的意思),所以查询结果也还会返回数据,且被认为是正确的查询结果。
解决这个问题很简单,我们只需要将传输器更换即可,Statement是低级的传输器,不安全,而且低效,更换为PreparedStatement高级传输器,会更加安全。
并且,我们在编写SQL语句时,也可以选择更加成熟的方案,使用?
代替参数的位置,?
叫占位符;其更加简洁,并且可以有效的避免SQL拼接参数可能带来的问题。
下面我们创建method2()
方法,将method1( )
方法中的内容复制过来,并修改其传输器及SQL语句:
private static void method2() throws Exception {
//1,注册驱动
Connection connection = JDBCUtils.getConnection();
//2,执行SQL
System.out.println("请输入用户名:");
String a = new Scanner(System.in).nextLine();//用户名
System.out.println("请输入密码:");
String b = new Scanner(System.in).nextLine();//密码
// //SQL骨架:用?代替了参数的位置,?叫占位符;好处:简洁(避免了SQL拼接参数)
String sql="select * from tb_user where name= ? and password= ?" ;
//获取传输器
PreparedStatement s = connection.prepareStatement(sql);
//ResultSet r = s.executeQuery(sql);
/*因为更换了新的传输器,SQL将作为参数传入到新的传输器中使用,
并且使用占位符重新编写了SQL语句,所以需要给占位符传入参数
我们使用其对象s中的setxxx方法传入参数*/
s.setString(1,a);//括号中需要俩个参数,第一个是位置,第二个是值
s.setString(2,b);//?的索引,要给?设置的值
ResultSet r = s.executeQuery();
//5,解析结果集
if(r.next()){//查到数据了吗?查到了就登录成功
System.out.println("登录成功~");
}else{
System.out.println("用户名或者密码输入错误,登录失败~");
}
//6,关闭资源
r.close();
s.close();
connection.close();
}
更换传输器后,我们需要将SQL语句作为参数传入新的传输器对象中进行使用;并且我们使用占用符更新了SQL,所以需要传输器对象 s 的setxxx方法,明确表明占位符接受的参数是哪个。
此时,便已经解决了SQL注入导致的攻击问题。
5.JDBC常见问题
5.1 驱动版本
不同版本的mysql需要对应不同版本的驱动,版本不对应会导致驱动无法正确运行而报错
Mysql5.0x mysql-connector-java-5.1.32.jar
Mysql8.0x mysql-connector-java-8.0.21.jar
5.2 中文乱码
url增中加参数:characterEncoding=utf8可以防止中文乱码
String url ="jdbc:mysql://localhost:3306/mydb?characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false";
## 数据库的名字 解决中文乱码 指定时区 关闭权限检验
5.3 PreparedStatement 语句
SQL注入解决方案:
Statement对象换为PreparedStatement对象
sql = "select * from teachers where tname=?"; #参数使用问号
PreparedStatement stat = cn.prepareStatement(sql); #对象换掉
stat.setString(1, condition); #对应参数类型,第几个问号
ResultSet rs = stat.executeQuery(); #去掉sql参数
PS后的结果:
SELECT * FROM teachers WHERE tname='陈强\' or 1=1 or \''
利用转义字符,屏蔽了SQL中的恶意字符。不仅解决了sql注入问题,使系统变的安全,PreparedStatement还有个极大的好处,它是预编译的语句,其主干部分mysql进行预编译后缓存,下次这部分就无需在解析,只把条件拼入,这样执行效率远高于statement每次都要编译sql语句。
5.4 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
错误原因:
-
jar没有导入,没有builder path
-
Class.forName(“com.mysql.jdbc.Driver”); 字符串拼写错误
5.5 Unknown database mydb;
错误原因:
- 数据库名称拼写错误
5.6 Access denied for user ‘root123’@‘localhost’ (using password: YES)
错误原因:
数据库用户名或者密码错误
5.7 Table ‘py-school-db.mydb’ doesn’t exist
错误原因:
表不存在,也可能表名写错了