JDBC学习笔记
JDBC【8.21】
JDBC是什么?
- Java DataBase Connectivity(Java语言连接数据库)
- 也就是JDBC是java提供的操作数据库的接口(规范)。
JDBC的本质是什么?
- JDBC是SUN公司制定的一套接口(interface)
- java.sql.*; (这个软件包下有很多接口)
- 接口都有调用者和实现者。
- 面向接口调用、面向接口写实现类,这都属于面向接口编程。
为什么要面向接口编程?
解耦合:降低程序的耦合度,提高程序的扩展力。
多态机制就是非常典型的:面向抽象编程。(不要面向具体编程)
建议:
Animal a = new Cat(); Animal a = new Dog(); //喂养的方法 public void feed(Animal a){ // 面向父类型编程。 }
思考:为什么SUN制定一套JDBC接口呢?
- 因为每一个数据库的底层实现原理都不一样。
Oracle数据库有自己的原理。
MySQL数据库也有自己的原理。
MS SqlServer数据库也有自己的原理。
。。。
每一个数据库产品都有自己独特的实现原理。
JDBC 的原理
- 早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的规范命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!
程序员,JDBC,JDBC驱动的关系
三方关系
SUN公司是规范制定者,制定了规范JDBC(连接数据库规范)
数据库厂商微软、甲骨文等分别提供实现JDBC接口的驱动jar包
程序员学习JDBC规范来应用这些jar包里的类。
总结:
–> 简单地说,JDBC 可做三件事:与数据库建立连接、发送指令操作数据库并处理结果。
JDBC操作数据库的步骤
口诀:加连欲执释
/*
* JDBC操作数据库的步骤: 必须掌握
* 贾: 加载驱动
* 琏: 获取数据库连接
* 欲: 获取语句对象
* 执: 执行语句
* 事: 释放资源
*
* 注意:操作数据库之前需要添加驱动jar包
*/
总体步骤
官网下载驱动包
加载一个Driver驱动
加载JDBC驱动是通过调用方法
java.lang.Class.forName()
注意,如果你的mysql版本是8或者以上的话,那么driver的写法需要改写成:com.mysql.cj.jdbc.Driver
创建数据库连接(Connection)
与数据库建立连接的方法是调用
DriverManager.getConnection(String url, String user, String password ) //eg: Connection conn=null; String url="jdbc:mysql://localhost:3306/bjpowernode?charsetUnicode=UTF8&serverTimezone=UTC"; String user=“root"; String password=“root"; conn = DriverManager.getConnection(url, user, password);
创建SQL命令发送器Statement
Statement对象用于将 SQL 语句发送到数据库中,或者理解为执行sql语句
有三种 Statement对象:
Statement:用于执行不带参数的简单SQL语句;
*PreparedStatement(从 Statement 继承):用于执行带或不带参数的预编译SQL语句;*
CallableStatement(从PreparedStatement 继承):用于执行数据库存储过程的调用。
创建SQL
通过Statement发送SQL命令并得到结果
处理ResultSet结果
ResultSet对象是executeQuery()方法的返回值,它被称为结果集,它代表符合SQL语句条件的所有行,并且它通过一套getXXX方法(这些get方法可以访问当前行中的不同列)提供了对这些行中数据的访问。
ResultSet里的数据一行一行排列,每行有多个字段,且有一个记录指针,指针所指的数据行叫做当前数据行,我们只能来操作当前的数据行。我们如果想要取得某一条记录,就要使用ResultSet的next()方法 ,如果我们想要得到ResultSet里的所有记录,就应该使用while循环。
ResultSet对象自动维护指向当前数据行的游标。每调用一次next()方法,游标向下移动一行。
初始状态下记录指针指向第一条记录的前面,通过next()方法指向第一条记录。循环完毕后指向最后一条记录的后面。
处理SQL结果(select语句)
关闭数据库资源
语法:
public void close() throws SQLException
ResultSet
Statement
Connection
用户不必关闭ResultSet。当它的 Statement 关闭、重新执行或用于从多结果序列中获取下一个结果时,该ResultSet将被自动关闭。
注意:要按先ResultSet结果集,后Statement,最后Connection的顺序关闭资源,因为Statement和ResultSet是需要连接是才可以使用的,所以在使用结束之后有可能其他的Statement还需要连接,所以不能先关闭Connection。
准备工作
创建数据库及数据表
- 在navicat Premium 15或者dos窗口中创建一个自己的数据库
- 在创建的数据库中创建自己的数据表
创建项目
- 在idea中新建一个自己的项目
创建lib目录并引入MySQL驱动包
在项目下新建一个lib目录 –> 与src同级
在目录中引入MySQL驱动包
在把lib包引入项目环境中 –> 选中 jar 包 –> 右击 –> add library
使用JDBC完成数据的添加操作
步骤
- 加载MySQL的JDBC驱动
- 建立数据的连接
- 创建SQL命令的发送器
- 编写SQL
- 使用SQL命令发送器发送SQL命令并得到结果
- 处理结果
- 关闭数据库资源
演示代码
package com.abc.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class Test01Add {
// 驱动器路径
private static final String DRIVER = "com.mysql.jdbc.Driver";
//连接数据库地址
private static final String URL = "jdbc:mysql://localhost:3306/whpowernode?useUnicode=true&useSSL=false&characterEncoding=UTF8";
//数据库用户名
private static final String USER_NAME = "root";
//数据库密码
private static final String USER_PASSWORD = "123456";
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 加载JDBC访问Oracle的驱动
Class.forName(DRIVER);
// 建立和数据库的连接
Connection conn = DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD);
// 创建SQL命令发送器
Statement stmt = conn.createStatement();
// 使用SQL命令发送器发送SQL命令并得到结果
String sql = "insert into student values(1,'小刚',32,'男','湖北省武汉市')";
int n = stmt.executeUpdate(sql);
// 处理结果
if (n > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
// 关闭数据库资源
stmt.close();
conn.close();
}
}
解决jdbc传入数据库时中文乱码问题
在java语句connect中添加字符编码集:
Connection conn = DriverManager.getConnection("jdbc:mysql:///bjpowernode?useUnicode=true&characterEncoding=UTF-8", "root", "123456");
URL详解
为什么要定义URL
- Java和MySQL是厂商的,Java程序和MySQL数据库此时不在同一个进程下,此时Java程序需要向MySQL发送请求。
如何发送请求
jdbc:mysql://localhost:3306/whpowernode?useUnicode=true&useSSL=false&characterEncoding=UTF8
使用URL的方式发送
jdbc 主协议
mysql 子协议
localhost MySQL服务器的地址,如果服务器就是我自己的主机,那么定义localhost就可以了
3306 MySQL服务器的端口号
whpowernode MySQL数据库服务器的数据库名称
useUnicode=true Java和MySQL交互使用Unicode编码
useSSL=false Java和MySQL交互不使用安全层协议
characterEncoding=UTF8 Java和MySQL交互的编码方式为UTF8 *【如果不设置会有乱码的】*
一个URL由哪些部分组成
协议://服务器主机:端口/服务器路径?查询参数
协议 jdbc:mysql:
服务器主机 localhost
端口 3306
服务器路径 whpowernode
参数useUnicode=true&useSSL=false&characterEncoding=UTF8
/*
* jbdc中数据库url的格式:
* jdbc:子协议://host:port/database
* MySQL的url格式:
* jdbc:mysql://host:port/database
*
* jdbc:mysql://localhost:3306/db01
* 如果url的host:port是localhost:3306,那么可以省略不写
* jdbc:mysql:///db01
*/
使用JDBC完成更新和删除操作
修改数据
代码
package com.abc.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class Test02Update { // 驱动器路径 private static final String DRIVER = "com.mysql.jdbc.Driver"; //连接数据库地址 private static final String URL = "jdbc:mysql://localhost:3306/whpowernode?useUnicode=true&useSSL=false&characterEncoding=UTF8"; //数据库用户名 private static final String USER_NAME = "root"; //数据库密码 private static final String USER_PASSWORD = "123456"; public static void main(String[] args) throws ClassNotFoundException, SQLException { // 加载Oracle的JDBC驱动 Class.forName(DRIVER); // 建立数据的连接 Connection conn=DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD); // 创建SQL命令的发送器 Statement stat=conn.createStatement(); // 编写SQL String sql="update student set name='小明',age=23,sex='女',address='武汉' where id=1"; // 使用SQL命令发送器发送SQL命令并得到结果 int res=stat.executeUpdate(sql); // 处理结果 if(res>0){ System.out.println("修改成功"); } else{ System.out.println("处理失败"); } // 关闭数据库资源 stat.close(); conn.close(); } }
删除数据
代码
package com.abc.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class Test03Delete { // 驱动器路径 private static final String DRIVER = "com.mysql.jdbc.Driver"; //连接数据库地址 private static final String URL = "jdbc:mysql://localhost:3306/whpowernode?useUnicode=true&useSSL=false&characterEncoding=UTF8"; //数据库用户名 private static final String USER_NAME = "root"; //数据库密码 private static final String USER_PASSWORD = "123456"; public static void main(String[] args) throws ClassNotFoundException, SQLException { // 加载Oracle的JDBC驱动 Class.forName(DRIVER); // 建立数据的连接 Connection conn=DriverManager.getConnection(URL, USER_NAME, USER_PASSWORD); // 创建SQL命令的发送器 Statement stat=conn.createStatement(); // 编写SQL String sql="delete from student where id=1"; // 使用SQL命令发送器发送SQL命令并得到结果 int res=stat.executeUpdate(sql); // 处理结果 if(res>0){ System.out.println("删除成功"); } else{ System.out.println("删除失败"); } // 关闭数据库资源 stat.close(); conn.close(); } }
DBUtils的简单封装
封装
我们为什么要封装,从以上代码可以看出,每一次写我们创建一个连接,创建一个发送SQL的对象,最后还要关闭,那么我们可以考虑把这重复的代码提取出来!
利用配置文件封装代码
package com.powernode.utils; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; /** * 工具类 */ public class DBUtils { private static String driverClassName; private static String dbUrl; private static String dbUsername; private static String dbPassword; private DBUtils(){} static { try { // 加载配置文件 Properties prop = new Properties(); prop.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties")); //通过key获取value driverClassName = prop.getProperty("className"); dbUrl = prop.getProperty("url"); dbUsername = prop.getProperty("username"); dbPassword = prop.getProperty("password"); //加载驱动 Class.forName(driverClassName); } catch (Exception e) { e.printStackTrace(); } } /** * 获取数据库连接 * @return 返回连接对象 */ public static Connection getConnection() throws SQLException { return DriverManager.getConnection(dbUrl, dbUsername, dbPassword); } /** * 释放资源 * @param closeables 资源 */ public static void close(AutoCloseable... closeables){ if(closeables != null){ try { for (AutoCloseable closeable : closeables) { closeable.close(); } } catch (Exception e) { e.printStackTrace(); } } } }
db.properties
# JDBC\u56DB\u8981\u7D20 # \u9A71\u52A8 className=com.mysql.cj.jdbc.Driver # url url=jdbc:mysql:///db01?rewriteBatchedStatements=true # \u7528\u6237\u540D username=root # \u5BC6\u7801 password=123456
url=jdbc:mysql:///db01?rewriteBatchedStatements=true :批处理要在配置文件中添加这一行
新建properties文件流程:
CRUD【增删查改】借用封装后的DBUtils工具类
import com.powernode.bean.Student;
import com.powernode.utils.DBUtils;
import org.junit.Test;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* CRUD: 增删查改
*/
public class CRUDDemo01 {
@Test
public void testInsert(){
Connection connection = null;
Statement statement = null;
try {
// 贾琏
connection = DBUtils.getConnection();
// 欲
statement = connection.createStatement();
// 执
String sql = "insert into db01.student values(0,1004,'java',20,'male','北京',default,default)";
// DML语句返回的是受影响的行数
int rows = statement.executeUpdate(sql);
System.out.println(rows);
} catch (Exception e) {
e.printStackTrace();
}finally {
DBUtils.close(statement,connection);
}
}
@Test
public void testDelete(){
Connection connection = null;
Statement statement = null;
try {
// 贾琏
connection = DBUtils.getConnection();
// 欲
statement = connection.createStatement();
// 执
String sql = "delete from student where sid = 1004";
// DML语句返回的是受影响的行数
int rows = statement.executeUpdate(sql);
System.out.println(rows);
} catch (Exception e) {
e.printStackTrace();
}finally {
DBUtils.close(statement,connection);
}
}
@Test
public void testUpdate(){
Connection connection = null;
Statement statement = null;
try {
// 贾琏
connection = DBUtils.getConnection();
// 欲
statement = connection.createStatement();
// 执
String sql = "update student set gender = 'female' where name = '李四'";
// DML语句返回的是受影响的行数
int rows = statement.executeUpdate(sql);
System.out.println(rows);
} catch (Exception e) {
e.printStackTrace();
}finally {
DBUtils.close(statement,connection);
}
}
@Test
public void testQuery(){
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
List<Student> students = new ArrayList<>();
try {
// 贾琏
connection = DBUtils.getConnection();
// 欲
statement = connection.createStatement();
// 执
String sql = "select * from student";
resultSet = statement.executeQuery(sql);
while (resultSet.next()){
// 将表中的数据解析成对象
//通过表字段名称获取其字段值 --> 与实体类的成员变量相对应
long id = resultSet.getLong("_id");
int sid = resultSet.getInt("sid");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
String gender = resultSet.getString("gender");
String province = resultSet.getString("province");
String create_time = resultSet.getString("create_time");
String update_time = resultSet.getString("update_time");
// 迭代表中一行数据,就对应一个实体对象
Student student = new Student(sid,name,age,gender,province);
student.set_id(id);
student.setCreate_time(create_time);
student.setUpdate_time(update_time);
students.add(student);
}
// 遍历集合
students.forEach(System.out::println);
} catch (Exception e) {
e.printStackTrace();
}finally {
DBUtils.close(resultSet,statement,connection);
}
}
}
Student.java
package com.powernode.bean; /** * 学生实体类,对应student表中的数据 * * 和数据表中对应的实体类的属性的数据类型使用包装类 * 因为包装类默认值为null,基本数据类型默认值不为null */ public class Student { private Long _id; private Integer sid; private String name; private Integer age; private String gender; private String province; private String create_time; private String update_time; public Student() { } public Student(Integer sid, String name, Integer age, String gender, String province) { this.sid = sid; this.name = name; this.age = age; this.gender = gender; this.province = province; } public Long get_id() { return _id; } public void set_id(Long _id) { this._id = _id; } public Integer getSid() { return sid; } public void setSid(Integer sid) { this.sid = sid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCreate_time() { return create_time; } public void setCreate_time(String create_time) { this.create_time = create_time; } public String getUpdate_time() { return update_time; } public void setUpdate_time(String update_time) { this.update_time = update_time; } @Override public String toString() { return "Student{" + "_id=" + _id + ", sid=" + sid + ", name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + ", province='" + province + '\'' + ", create_time=" + create_time + ", update_time=" + update_time + '}'; } }
使用JDBC完成查询
查询前先循环向student表中插入20条数据
package com.bjpowernode.jdbc;
import com.bjpowernode.utils.DBUtils;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Random;
public class Test01Add20 {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Connection conn = DBUtils.getConn();
// 创建SQL命令发送器
Statement stmt = conn.createStatement();
// 使用SQL命令发送器发送SQL命令并得到结果
Random random=new Random();
for (int i = 1; i <=20 ; i++) {
Integer id=i;
String name="小明"+i;
int age=random.nextInt(100);
String sex=random.nextBoolean()?"男":"女";
String address="武汉"+i;
String sql = "insert into student values("+i+",'"+name+"',"+age+",'"+sex+"','"+address+"')";
int n = stmt.executeUpdate(sql);
// 处理结果
if (n > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
}
// 关闭数据库资源
DBUtils.close(stmt);
DBUtils.close(conn);
}
}
JDBC查询
package com.abc.jdbc;
import com.abc.utils.DBUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Test04Query {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Connection conn = DBUtils.getConn();
// 创建SQL命令发送器
Statement stmt = conn.createStatement();
// 编写SQL
String sql="select * from student";
// 使用SQL命令发送器发送SQL命令并得到结果
ResultSet rs=stmt.executeQuery(sql);
// 处理结果
while(rs.next()){
int id=rs.getInt(1);
String name=rs.getString(2);
int age=rs.getInt(3);
String sex=rs.getString(4);
String address=rs.getString(5);
System.out.println(id+" "+name+" "+age+" "+sex+" "+address);
}
// 关闭数据库资源
DBUtils.close(rs);
DBUtils.close(stmt);
DBUtils.close(conn);
}
}
使用JDBC完成分页查询
import com.bjpowernode.utils.DBUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Test05QueryForPage {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Connection conn = DBUtils.getConn();
// 创建SQL命令发送器
Statement stmt = conn.createStatement();
int pageNum=2; //页码
int pageSize=5;//每页显示的条数
// 编写SQL
String sql="select * from student limit "+(pageNum-1)*pageSize+","+pageSize;
// 使用SQL命令发送器发送SQL命令并得到结果
ResultSet rs=stmt.executeQuery(sql);
// 处理结果
while(rs.next()){
int id=rs.getInt(1);
String name=rs.getString(2);
int age=rs.getInt(3);
String sex=rs.getString(4);
String address=rs.getString(5);
System.out.println(id+" "+name+" "+age+" "+sex+" "+address);
}
// 关闭数据库资源
DBUtils.close(rs);
DBUtils.close(stmt);
DBUtils.close(conn);
}
}
JDBC常用接口详解(位于java.sql包下)
- Driver 接口:
- 每个驱动程序类必须实现的接口。也就是说数据库厂商提供的驱动程序必须实现该接口。
- DriverManager类:
- 驱动管理类。管理一组JDBC驱动程序的基本服务。
- Connection接口:
- 与特定数据库的连接(会话)。在连接上下文中执行SQL语句并返回结果。
- Statement接口:
- 用于执行静态SQL语句并返回它所生成结果的对象。
- preparedStatement接口:
- 预编译的Statement对象,PreparedStatement是Statement的子接口,它允许数据库预编译sql语句(这些sql语句通常带有参数),以后每次只改变sql命令的参数,避免数据库每次都需要编译sql语句,无需再传入sql语句。
只要为预编译的sql语句传入参数值即可。所以它比Statement多了如下方法:
void setXxx(int parameterIndex, Xxx value):该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给sql语句中指定位置的参数。
- 预编译的Statement对象,PreparedStatement是Statement的子接口,它允许数据库预编译sql语句(这些sql语句通常带有参数),以后每次只改变sql命令的参数,避免数据库每次都需要编译sql语句,无需再传入sql语句。
- ResultSet接口:
- 表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。
- ResultSet 对象具有指向其当前数据行的光标。最初,光标被放置于第一行之前。
- next 方法将光标移动到下一行:因为该方法在ResultSet 对象没有下一行时返回false,所以可以在while循环中使用它来迭代结果集。
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
/**
* JDBC操作数据库的步骤: 必须掌握
* 贾: 加载驱动
* 琏: 获取数据库连接
* 欲: 获取语句对象
* 执: 执行语句
* 事: 释放资源
*
* 注意:操作数据库之前需要添加驱动jar包
*/
public class Demo01 {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 贾: 加载驱动
/*
* 通过查看Driver类的源码发现:
* static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
Driver类中已经执行了DriverManager.registerDriver(new Driver());,所以我们没有必要
再编写DriverManager.registerDriver(new Driver()); 只需要调用Driver类中的即可。
* Driver中使用的是静态代码块,所以只需要加载Driver类即可。
*/
// DriverManager.registerDriver(new Driver());
/*
* Class.forName("com.mysql.cj.jdbc.Driver");就会将"com.mysql.cj.jdbc.Driver"字节码加载进内存
* 所以Driver中使用的是静态代码块就会被执行。
*
* 注意:JDBC 4.0 Drivers 必须包括 META-INF/services/java.sql.Driver 文件。
* 此文件包含 java.sql.Driver 的 JDBC 驱动程序实现的名称。
* 应用程序不再需要使用 Class.forName() 显式地加载 JDBC 驱动程序。
*/
Class.forName("com.mysql.cj.jdbc.Driver");
// 琏: 获取数据库连接
/*
* jbdc中数据库url的格式:
* jdbc:子协议://host:port/database
* MySQL的url格式:
* jdbc:mysql://host:port/database
*
* jdbc:mysql://localhost:3306/db01
* 如果url的host:port是localhost:3306,那么可以省略不写
* jdbc:mysql:///db01
*/
connection = DriverManager.getConnection("jdbc:mysql:///db01", "root", "123456");
// 欲: 获取语句对象
statement = connection.createStatement();
// 执: 执行语句
/**
* DML语句使用:executeUpdate()
* DQL语句使用:executeQuery()
*/
String sql = "select sid,name,age from student";
/*
* ResultSet表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。
*/
resultSet = statement.executeQuery(sql);
// 解析结果集数据
while(resultSet.next()){
/*
* resultSet获取数据的时候都有两个重载的方法,分别是:
* getXXX(int 投影字段的索引)
* 使用索引的弊端: 就是投影字段的顺序变化后就会出问题
* getXXX(String 投影字段的名字) -- 推荐使用
*
*/
String name = resultSet.getString("name");
System.out.println(name);
}
} catch (Exception throwables) {
throwables.printStackTrace();
}finally {
// 事: 释放资源,后用的先释放
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
resultSet = null;
}
if(statement != null){
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
statement = null;
}
if(connection != null){
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
connection = null;
}
}
}
}
SQL注入问题
SQL注入就是sql中接收的用户参数破坏了原有的sql语句。
select * from t_user where username = 'xxx' and password = '' or '1=1'
- 上述代码就相当于是将 or 关键字也参加了sql语句的编译,where条件过滤的时候,先判断 and 条件,然后不管是否是真,当执行 or 时,结果都是真,就相当于破解了密码了。
使用Statement操作动态SQL就会有sql注入的风险
为什么有sql注入的风险?
–> 因为接收的参数也参与sql语句的编译,所以参数中如果带有sql关键字,就可以破坏原有的sql语句。
解决思路:
–> 先编译原有的sql,再使用参数,此时参数就不会参与到sql编译中,所以即使参数中出现了关键字,也只会当作普通参数使用了。
jdbc 中提供了Statement接口的子接口 PreparedStatement 来解决SQL注入问题。
PreparedStatement 接口:
表示预编译的SQL语句的对象
SQL语句被预编译并存储再PreparedStatement 对象中。然后可以使用此对象多次高效地执行该语句。
实例代码:
import com.powernode.utils.DBUtils; import java.sql.*; import java.util.Scanner; //登录案例,演示动态SQL,也就是参数不固定 public class LoginDemo01 { public static void main(String[] args) { Connection connection = null; PreparedStatement ps = null; ResultSet resultSet = null; try { Scanner scanner = new Scanner(System.in); System.out.println("请输入用户名:"); String name = scanner.nextLine(); System.out.println("请输入密码:"); String pwd = scanner.nextLine(); connection = DBUtils.getConnection(); /* * ? 表示占位符,也就是?处在sql语句编译后需要使用具体值替换 */ String sql = "select * from t_user where username = ? and password = ?"; // 预编译sql,并保存到PreparedStatement对象中 ps = connection.prepareStatement(sql); // 预编译完成后,将?占位符的值替换成实际值 /* * setString(int parameterIndex, String x): * 第一个参数: 表示?的索引位置,从1开始 * 第二个参数: 就是实际值 */ ps.setString(1,name); ps.setString(2,pwd); System.out.println(sql); /* * 因为 ps对象中存储了SQL语句,所以执行方法中不传入sql */ resultSet = ps.executeQuery(); if(resultSet.next()){ String username = resultSet.getString("username"); System.out.println("欢迎"+ username); }else{ System.out.println("账号或密码错误"); } } catch (Exception e) { e.printStackTrace(); }finally { DBUtils.close(resultSet,ps,connection); } } /* * 存在SQL注入风险 */ public static void test(){ Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { Scanner scanner = new Scanner(System.in); System.out.println("请输入用户名:"); String name = scanner.nextLine(); System.out.println("请输入密码:"); String pwd = scanner.nextLine(); connection = DBUtils.getConnection(); statement = connection.createStatement(); String sql = "select * from t_user where username = '" + name + "' and password = '" + pwd +"'"; System.out.println(sql); resultSet = statement.executeQuery(sql); if(resultSet.next()){ String username = resultSet.getString("username"); System.out.println("欢迎"+ username); }else{ System.out.println("账号或密码错误"); } } catch (Exception e) { e.printStackTrace(); }finally { DBUtils.close(resultSet,statement,connection); } } }
事务处理解决转帐问题
注意: 事务是通过Connection连接对象获取的
import com.powernode.utils.DBUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; /** * 事务: * 转账的案例: 张三给李四转账100元 * * 注意: 事务是通过Connection连接对象获取的 */ public class TransactionDemo01 { public static void main(String[] args) { Connection connection = null; PreparedStatement ps = null; try { connection = DBUtils.getConnection(); // 关闭事务的自动提交,事务通过手动提交 connection.setAutoCommit(false); // 开启事务 // 张三的账户减少100元 String sql = "update account set money = money - ? where name = ? "; ps = connection.prepareStatement(sql); ps.setInt(1,100); ps.setString(2,"zhangsan"); ps.executeUpdate(); //如果没有添加事务,此处发生异常,下面的代码也不会执行,则就会发生转账金额丢失,前后数据不一致的情况。 int i = 1 / 0; // 李四的账户增加100元 sql = "update account set money = money + ? where name = ? "; ps = connection.prepareStatement(sql); ps.setInt(1,100); ps.setString(2,"lisi"); ps.executeUpdate(); // 手动提交事务 connection.commit(); } catch (SQLException throwables) { // 发生异常后,回滚事务 try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } throwables.printStackTrace(); }finally { DBUtils.close(ps,connection); } } }
JDBC批处理
- 批处理: 批量执行多个DML语句
- url=jdbc:mysql:///db01?rewriteBatchedStatements=true :批处理需要用到要在配置文件中添加这一行
使用批处理出入3000条数据
注意: 插入3000条数据,只是数据不同,所以语句只需要写一个,只需要预编译一次,后面反复使用
import com.powernode.utils.DBUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; /** * 批处理: 批量执行多个DML语句 */ public class BatchDemo01 { public static void main(String[] args) { // noUseBatch(); useBatch(); } /** * 使用批处理插入3000条数据 * * 注意: 插入3000条数据,只是数据不同,所以语句只需要写一个,只需要预编译一次,后面反复使用 */ private static void useBatch() { Connection connection = null; PreparedStatement ps = null; try { connection = DBUtils.getConnection(); connection.setAutoCommit(false); //这里sql语句不能添加; 否则会报错 String sql = "insert into account values (0,?,?)"; ps = connection.prepareStatement(sql); long start = System.currentTimeMillis(); for (int i = 0; i < 3000; i++) { ps.setString(1,"java" + i); ps.setInt(2,100+i); ps.addBatch(); // 添加在批处理 } ps.executeBatch(); // 一次性执行批量数据 connection.commit(); long end = System.currentTimeMillis(); // 耗时:597 System.out.println("耗时:" + (end-start)); } catch (SQLException throwables) { try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } throwables.printStackTrace(); }finally { DBUtils.close(ps,connection); } } /** * 没有使用批处理插入3000条数据 * * 注意: 插入3000条数据,只是数据不同,所以语句只需要写一个,只需要预编译一次,后面反复使用 */ private static void noUseBatch() { Connection connection = null; PreparedStatement ps = null; try { connection = DBUtils.getConnection(); connection.setAutoCommit(false); //这里的第一个参数为0的话必须要在mysql中将它设置为自动递增,否则报错 String sql = "insert into account values (0,?,?)"; ps = connection.prepareStatement(sql); long start = System.currentTimeMillis(); for (int i = 0; i < 3000; i++) { ps.setString(1,"java" + i); ps.setInt(2,100+i); ps.executeUpdate(); } connection.commit(); long end = System.currentTimeMillis(); // 耗时:2283 System.out.println("耗时:" + (end-start)); } catch (SQLException throwables) { try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } throwables.printStackTrace(); }finally { DBUtils.close(ps,connection); } } }
db.properties配置文件
className = com.mysql.cj.jdbc.Driver url = jdbc:mysql:///db02?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true userName = root password = 123456
连接池
连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。连接池是装有连接的容器,使用连接的话,可以从连接池中进行获取,使用完成之后将连接归还给连接池。
DataSource 接口:
- java中提供的连接池接口。
- 作为DriverManager 工具的替代项,DataSource 对象是获取连接的首选方法。
DataSource 接口由驱动程序供应商实现。共有三种类型的实现:
- 基本实现 - 生成标准的Connection 对象
- 连接池实现 - 生成自动参与连接池的 Connection 对象。
- 分布式事务实现 - 生成一个Conneciton 对象,该对象可用于分布式事务,大多数情况下总是参与连接池。
Druid 德鲁伊连接池
为监控而生,内置强大的监控功能,监控特性不影响性能,功能强大,能防sql注入,内置Loging能诊断hack应用行为。
在idea中使用Druid工厂要添加druid 的 jar 包。
import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.IOException; import java.sql.Connection; import java.util.Properties; public class DruidDemo01 { public static void main(String[] args) throws Exception { // 获取连接池对象 Properties properties = new Properties(); properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("druid.properties")); DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); //从连接池里获取连接对象s Connection connection = dataSource.getConnection(); // 以下的代码和之前操作一样 } }
druid.properties配置文件【要根据druid官方的来配置,不能自己随意配置(key)】
# driverClassName可配可不配,会自动根据url识别 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql:///db02?rewriteBatchedStatements=true username=root password=123456
DruidUtils封装
package com.cdpowernode.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.util.Properties;
/**
* @Author 小白不白
* @Description:
* @Version 1.0
*/
public class DruidUtils {
//定义成员变量
private static Properties properties;
private DruidUtils() {
}
static{
//加载配置文件
properties = new Properties();
try {
properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("druid.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 创建连接池对象并从连接池中获取连接对象
* @return
*/
public static Connection getConnection() throws Exception {
//线程池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
//从线程池中获取连接对象
return dataSource.getConnection();
}
/**
* 释放资源
* 注意传入的连接如果是从连接池中获取的,那么释放连接就是归还到连接池中
* @param closeables 资源
*/
public static void close(AutoCloseable ... closeables){
if(closeables != null){
try {
for(AutoCloseable closeable : closeables){
closeable.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
BaseDAO的封装【难点】
DAO: Data Access Object 数据访问对象,DAO 就是用来操作数据库的。
import com.powernode.bean.Student; import com.powernode.utils.DruidUtils; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; /** * DAO: Data Access Object 数据访问对象 * 也就是说DAO 就是用来操作数据库的 */ public class BaseDao { /** * DML语句操作 * * @param sql DML语句 * @param args 参数 * @return 返回受影响的行数 */ public int update(String sql, Object... args) { Connection connection = null; PreparedStatement statement = null; try { connection = DruidUtils.getConnection(); statement = connection.prepareStatement(sql); for (int i = 0; i < args.length; i++) { statement.setObject(i + 1, args[i]); } return statement.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } finally { DruidUtils.close(statement, connection); } return 0; } /** * DQL语句 * * @param cls 封装的实体类 * @param sql sql语句 * @param args 参数 * @param <T> 实体类泛型 */ public <T> List<T> query(Class<T> cls, String sql, Object... args) { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; List<T> list = new ArrayList<>(); try { connection = DruidUtils.getConnection(); statement = connection.prepareStatement(sql); for (int i = 0; i < args.length; i++) { statement.setObject(i + 1, args[i]); } resultSet = statement.executeQuery(); /* * Java实体类和表中的数据是对应的,所以一般实体类的成员变量名称和字段的名称是对应的。 * 所以我们可以通过表字段的名称 使用反射的技术 获取到实体类的成员变量。 * * resultSet对象中就是查询的结果集数据,所以可以通过resultSet获取字段数据 * * 注意: 使用以下的方法前提条件是: * 实体类的成员变量的数据类型要和表中字段的数据类型匹配。 * 表中字段的数据类型是无符号的int,实体类中需要使用Long */ // 获取元数据对象 ResultSetMetaData metaData = resultSet.getMetaData(); // 获取投影字段的数量 int columnCount = metaData.getColumnCount(); // while循环一次获取到一行数据 while (resultSet.next()) { T obj = cls.newInstance(); for (int i = 1; i <= columnCount; i++) { // 获取表投影字段的名称 // String columnName = metaData.getColumnName(i); /* getColumnLabel(): 如果字段没有别名就获取字段名称;如果有别名就获取别名 可以通过别名的方式解决实体类的成员变量和表字段名称不相同的问题 */ String columnName = metaData.getColumnLabel(i); // 通过表字段的名称 使用反射的技术 获取到实体类的成员变量 Field declaredField = cls.getDeclaredField(columnName); // 给对象的字段赋值 declaredField.setAccessible(true); Object value = resultSet.getObject(i); declaredField.set(obj, value); } list.add(obj); } } catch (Exception e) { e.printStackTrace(); } finally { DruidUtils.close(resultSet, statement, connection); } return list; } /** * 分页查询 * @param cls 封装的实体类 * @param sql DQL语句 * @param page 第几页 * @param limitNum 每页显示的数量 * @param <T> 实体类 * @return PageInfo对象 */ public <T> PageInfo<T> queryPage(Class<T> cls, String sql, int page, int limitNum) { int skip = 0; //(第几页 -1) * 每页显示的数据条数 skip = (page - 1) * limitNum; //这里的拼接一定要注意空格 String pageSql = sql + " limit " + skip + "," + limitNum; // 查询得到指定页的数据 List<T> list = query(cls, pageSql); // 查询总数据条数 Long count = getCount(sql); PageInfo<T> pageInfo = new PageInfo<>(); pageInfo.setData(list); pageInfo.setCount(count); return pageInfo; } /** * 查询总数据条数 * * @param sql * @return */ private Long getCount(String sql) { try { /* * 这是一个子查询 * sql = select * from student; * countSql = select count(1) from (select * from student) rs; */ //欲执行的sql语句 from后面用子查询一定要取别名 String countSql = "select count(1) from (" + sql + ") rs"; Connection conn = DruidUtils.getConnection(); PreparedStatement prep = null; ResultSet rs = null; prep = conn.prepareStatement(countSql); rs = prep.executeQuery(); //初始光标在字段名称前面,当next()后就会指向第一条数据 rs.next(); Long count = rs.getLong(1); return count; } catch (Exception throwables) { throwables.printStackTrace(); } return 0L; } ; }
Student.java
import java.time.LocalDateTime; /** * @Author 小白不白 * @Date 2022/9/19 22:29 * @Description: * @Version 1.0 */ public class Student { private Long _id; //对于mysql中无符号的int型在java中只能用Long类型 private Integer sid; private String name; private Integer age; private String gender; private String province; //进行对表字段反射的时候无法识别String类型,所以建议使用jdk1.8提供的LocalDataTime类 private LocalDateTime create_time; private LocalDateTime update_time; public Student() { } public Student(Integer sid, String name, Integer age, String gender, String province) { this.sid = sid; this.name = name; this.age = age; this.gender = gender; this.province = province; } public Long get_id() { return _id; } public void set_id(Long _id) { this._id = _id; } public Integer getSid() { return sid; } public void setSid(Integer sid) { this.sid = sid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public LocalDateTime getCreate_time() { return create_time; } public void setCreate_time(LocalDateTime create_time) { this.create_time = create_time; } public LocalDateTime getUpdate_time() { return update_time; } public void setUpdate_time(LocalDateTime update_time) { this.update_time = update_time; } @Override public String toString() { return "Student{" + "_id=" + _id + ", sid=" + sid + ", name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + ", province='" + province + '\'' + ", create_time=" + create_time + ", update_time=" + update_time + '}'; } }
PageInfo.java
import java.util.List; public class PageInfo<T> { private List<T> data;// 具体的数据 private Long count; //符合条件的数据条数 public List<T> getData() { return data; } public void setData(List<T> data) { this.data = data; } public Long getCount() { return count; } public void setCount(Long count) { this.count = count; } }
测试代码:
import java.util.List; /** * @Author 小白不白 * @Description: * @Version 1.0 */ public class DAOTestDemo { public static void main(String[] args) { BaseDAO baseDAO = new BaseDAO(); //DML语句 /*String sql = "insert into student values(5,1005,'柯南',23,'male','江户川',default,default)"; int i = baseDAO.update(sql); System.out.println(i);*/ //DQL语句 /*String sql = "select * from student"; List<Student> list = baseDAO.query(Student.class,sql); for (Student student : list) { System.out.println(student); }*/ //分页查询 String sql = "select * from student"; PageInfo<Student> studentPageInfo = baseDAO.queryPage(Student.class, sql, 1, 2); System.out.println(studentPageInfo.getCount()); List<Student> list = studentPageInfo.getData(); list.forEach(System.out :: println); } }
CRUD利用BaseDao封装工具
import org.junit.Test;
import java.util.List;
/**
* CRUD: 增删查改
*/
public class CRUDDemo01 {
@Test
public void testInsert(){
String sql = "insert into db01.student values(0,?,?,?,?,?,default,default)";
int rows = new BaseDao().update(sql, 2001, "java", 20, "male", "上海市");
System.out.println(rows);
}
@Test
public void testDelete(){
String sql = "delete from student where sid = ?";
int rows = new BaseDao().update(sql, 2001);
System.out.println(rows);
}
@Test
public void testUpdate(){
String sql = "update student set gender = ? where name = ?";
int rows = new BaseDao().update(sql, "female","李四");
System.out.println(rows);
}
@Test
public void testQuery(){
String sql = "select * from student";
List<Student> list = new BaseDao().query(Student.class, sql);
list.forEach(System.out::println);
}
@Test
public void testQueryPage(){
String sql = "select * from student";
PageInfo<Student> pageInfo = new BaseDao().queryPage(Student.class, sql, 2, 2);
System.out.println("总条数: "+pageInfo.getCount());
List<Student> list = pageInfo.getData();
list.forEach(System.out::println);
}
}