JDBC
Java对数据库进行操作(增删改查)的技术
1、JDBC的基本使用
使用流程(增删改):
- 导入驱动包
- 获取连接对象
- 编写sql语句并由statement对象提交执行
- 关闭资源
//导入驱动包
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接对象
String url = "jdbc:mysql://localhost:3306/java2402?characterEncoding=utf8&serverTimezone=UTC";
String user = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, user, password);
//获取statement对象
Statement statement = connection.createStatement();
//编写sql语句+执行sql命令 -> 返回受影响的行数
String sql = "insert into student values (8,'李虹霖','女',20,8000,'Python', 1,2,3);";
//String sql = "delete from student where id = 8";
//String sql = "update student set salary = 1000 where id = 4";
int i = statement.executeUpdate(sql);
System.out.println("Affected rows: " + i);
//关闭连接对象
statement.close();
connection.close();
如果是查询:
- 导入驱动包
- 获取连接对象
- 编写sql语句并由statement对象提交执行,获得resultSet对象 -> 获取查询结果
- 关闭资源
//导入驱动包
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接对象
String url = "jdbc:mysql://localhost:3306/java2402?characterEncoding=utf8&serverTimezone=UTC";
String user = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, user, password);
//获取statement对象
Statement statement = connection.createStatement();
//编写sql语句+执行sql命令 -> 返回受影响的行数
String sql = "select * from student";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
double salary = resultSet.getDouble("salary");
String course = resultSet.getString("course");
int tId = resultSet.getInt("t_id");
int courseId = resultSet.getInt("course_id");
int classId = resultSet.getInt("class_id");
System.out.println(id + "-" + name + "-" + salary + "-" + course + "-" + courseId + "-" + classId + "-" + tId);
}
//关闭连接对象
resultSet.close();
statement.close();
connection.close();
2、结合反射查询数据并生成对象
假设一张表里面存储了学生的信息,在通过JDBC查询后生成一个对应的对象,那么就可以使用反射来实现
@Test
public void test04() throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
String sql = "select * from student where course = ?";
List<Student> list = DBManager.query(Student.class, sql, "JAVA");//其中在DBManager中封装了查询的主要逻辑
for (Student student : list) {
System.out.println(student);
}
}
//...
3、SQL注入
SQL注入问题:所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令
SELECT * FROM user where userName = '' or 1 = 1 # AND password =
上述的SQL注入命令就是'' or 1 = 1 #
,欺骗了服务器,从而达到注入的效果
解决方案:使用预编译
在生成statement
对象时,采用connection.perpareStatement(sql语句)
的方式,然后通过statement.setObject(inedx, value)
的方式对sql语句进行预编译处理,能够防止sql注入问题
Connection connection = DriverManager.getConnection();
String sql = "select * from student where name =?";//使用 ? 进行占位
PreparStatement statement = connection.preparStatement(sql);
statement.setString(1, "刘亦菲");//下标是从1开始,参数的类型按照实际情况使用对应的类型
resultSet results = statement.executeQuery();
if(result.next()){
String name = result.getString("name");
int age = result.getInt("age");
SYstem.out.println(name + age);
}
//关闭资源
result.close();
statement.close();
connection.close();
4、主键回传
主键回传是指在查询后返回的结果为表的主键,用于后续操作,查询过程与普通查询一致,只需要在获取statement对象时加上Statement.RETURN_GENERATED_KEYS
参数
preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
5、事务
事务是数据库操作的一个基本执行单元
//开启事务
connection.setAutoCommit(false);
//提交事务
connection.commit();
//事务回滚
connection.rollback();
在sql语句中开启事务、提交、回滚
start transaction;-- 开启事务
commit;-- 提交事务
rollback;-- 事务回滚
6、批处理
当执行重复性操作时,考虑批处理
Connection connection = DriverManager.getConnection();
String sql = "insert into student (name, age) values(?,21)";
PreparStatement statement = connection.preparStatement(sql);
for(int i=0;i<1000;i++){
statement.setString(1,"刘亦菲" +i);
statement.addBatch();
if(i%100 == 0 ){
statement.executeBatch();
statement.clearBatch();
}
}
//关闭资源...
7、面试题
-
数据库的特性(ACID)?
- 原子性:事务是数据库操作执行单元,要么全部执行成功,要么全部执行失败;对于一个事务来说,不可能只执行其中一部分sql操作,这就是事物的原子性
- 一致性:数据库总是从一个一致性状态到另一个一致性状态。
- 隔离性:通常来说,一个事务所做的修改在提交以前,其他事务是不可见的,不同的事务之间互不干扰。
- 持久性:一旦事务提交,所做的修改就会永久保存到数据库中,即使系统崩溃,修改的数据也不会丢失。
-
并发时出现的问题?
- 脏读:一个事务读取到了另一个事务中未提交的数据
- 不可重复读:一个事务先后读取同一条记录,而数据在事务两次读取之间被其他事务所修改,所以两次读取到的数据不一致
- 虚读:一个事务先后按照相同的查询条件重新读取以前检索过的数据,发现其他事务插入了满足查询条件的数据
脏读与不可重复读的区别:脏读读取到的是另一个事务未提交的数据,而不可重复读读取的是已经提交的数据,只是前后读取的数据被其他事务修改了
不可重复读与虚读的区别:不可重复读指的是同一条数据项,而虚读针对的是一批数据整体(比如个数变化)
-
隔离级别?
-
READ UNCOMMITED:脏读、不可重复读、虚读都可能发生
-
READ COMMITED:避免了脏读,不可重复读、虚读可能发生
-
REPEATABLE READ:避免了脏读和不可重复读,虚读可能发生
-
SERIALIZABLE:避免了脏读、不可重复读、虚读(一般来说,上一个隔离级别虚读基本就能避免虚读的问题)
//设置隔离级别(可重复读) connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
-
-
最高隔离级别存在的问题?
SERIALIZABLE:当一个事务提交之前,其他事务不能执行(类似于多线程的排他),不适用于高并发情况
8、连接池
连接池的优点:
- 减少了创建链接和关闭连接的时间和空间代价
- 使项目中的连接数可控
- 提高了连接的复用性
自定义连接池
思路:
- 创建一个连接池类,存放多个连接
- int 最大连接数
- LinkedList<Connection> list 容器
- String 驱动路径
- String 连接url
- String 用户名
- String 密码
- Connection getConnection() 获取连接对象 -> 获取单个连接
- 包装connection,修改close() 方法
- 测试
连接池
/**
* 自定义连接池
*/
@SuppressWarnings({"all"})
public class MyConnectionPool implements DataSource {
//驱动
private String driverName;
//url
private String url;
//username
private String name;
//password
private String password;
//容器
private LinkedList<Connection> list;
//最大连接数
private int maxActive;
/**
* 初始化连接池
*/
private void init() throws SQLException, IOException {
//初始化容器
list = new LinkedList<>();
try {
//加载驱动
Class.forName(driverName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
//向容器中初始化maxActive个连接
for (int i = 0; i < maxActive; i++) {
//获取mysql的connection对象
Connection connection = DriverManager.getConnection(url, name, password);
//将connection包装 -> 修改close()方法
MyConnectionWrapper wrapper = new MyConnectionWrapper(list, connection);
list.add(wrapper);
}
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public void setUrl(String url) {
this.url = url;
}
public void setName(String name) {
this.name = name;
}
public void setPassword(String password) {
this.password = password;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public String toString() {
return "MyConnectionPool{driverName = " + driverName + ", url = " + url + ", name = " + name + ", password = " + password + ", list = " + list + "}";
}
@Override
public Connection getConnection() throws SQLException {
//如果list中还没有连接,则初始化
if (list == null) {//这里不使用 list.isEmpty() 是因为 init() 中才初始化 LinkedList
try {
init();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//如果请求连接数为空,则等待
if(list.isEmpty()){
System.out.println("连接池已经没有空闲Connection,请等待");
return null;
}
return list.removeFirst();
}
@Override
...//其他需要重写的方法
}
包装类
/**
* 包装connection,修改connection的close()方法
* 返回的是被包装过后的 wrapper(类型仍然是 Connection)
*/
public class MyConnectionWrapper implements Connection {
private LinkedList<Connection> list;
private Connection connection;
public MyConnectionWrapper(LinkedList<Connection> list, Connection connection) {
this.list = list;
this.connection = connection;
}
//。。。其他重写方法
@Override
public void close() throws SQLException {
//修改默认 close() 方法
list.add(this);
}
}
测试
public static void main(String[] args) throws SQLException {
MyConnectionPool pool = new MyConnectionPool();
//添加配置
pool.setDriverName("com.mysql.cj.jdbc.Driver");
pool.setUrl("jdbc:mysql://localhost:3306/xxxx?characterEncoding=utf8&serverTimezone=UTC");
pool.setName("root");
pool.setPassword("xxxx");
pool.setMaxActive(20);
Connection connection = null;
PreparedStatement statement;
ResultSet res;
String sql = "select * from student where id < 5";
try {
connection = pool.getConnection();
statement = connection.prepareStatement(sql);
res = statement.executeQuery();
while (res.next()) {
int id = res.getInt("id");
String name = res.getString("name");
String sex = res.getString("sex");
System.out.println(id + "--" + name + "--" + sex);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
connection.close();
}
}
druid连接池
druid连接池有两种方式可以实现
- 创建druidDataSource对象,再通过setXxx()方法配置连接
- 通过DruidDataSourceFactory.createDataSource(properties)加载配置文件自动生成
//这里是第二种方法
//DBManager中的静态加载部分
Properties properties = new Properties();
properties.load(DBManager.class.getClassLoader().getResourceAsStream("DBConfig.properties"));
DataSource dataSource = dataSource = DruidDataSourceFactory.createDataSource(properties);
//DBManager是一个工具类,自定义封装了很多方法,包括查询query()、getConnection()等
//DBManager中的getConnection()方法
public static Connection getConnection() throws SQLException {
Connection connection = threadLocal.get();
if (connection == null) {
connection = dataSource.getConnection();
threadLocal.set(connection);
}
return connection;
}
//---------------------------------------------------------
String sql = "select * from student where id = ?";
List<Student> query = DBManager.query(Student.class, sql,1);
query.forEach(System.out::println);
DBConfig.properties
driverName = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/xxx?characterEncoding=utf8&serverTimezone=UTC
username = root
password = xxx
maxActive = 20
其他连接池
其他常见的连接池还包括
- C3P0:稳定性好
- DBPC:速度快
其中druid连接池是使用最多、效果最好的