回顾
1. jdbc基础
通过java语言操作数据库
快速入门
1.注册驱动
2.建立连接
3.编写sql
4.获取sql执行对象
5.执行sql并返回结果
6.处理结果
7.释放资源
API详解
DriverManager:驱动管理器
1.注册驱动
2.建立连接
Connection
1.获取sql执行对象
Statement
PreparedStatement
2.事务管理
Statement
1.执行DML类型
int executeUpdate(String sql);
2.执行DQL类型
ResultSet executeQuery(String sql);
ResultSet
1.指针下移
boolean next();
2.获取数据
T getXxx(String 列名);
CRUD案例
JdbcUtils工具类:简化书写,提高效率
事务管理
try{
1.关闭自动提交
2. 处理业务逻辑
3. 提交事务
}catch(Exception e){
4.回滚事务
}finally{
5.释放资源
}
2. 用户登录
JDBC&连接池
今日目标
1. PreparedStatement
2. 数据库连接池
3. 用户登录
三层架构+连接池技术实现用户登录
一 PreparedStatement【重点】
1.1 概述
SQL注入问题
我们让用户输入的信息和SQL语句进行字符串拼接。用户输入的内容作为了SQL语句语法的一部分,改变了原有SQL真正的意义,以上问题称为SQL注入。
-- 这条sql语句原有的含义是根据用户名和密码查询
-- 现在用户输入了一些特殊字符,改变了sql原有的含义,这种行为成为sql注入
SELECT * FROM USER WHERE username ='admin'# ' and password =''
解决sql注入问题
我们就不能让用户输入的信息和SQL语句进行字符串拼接。需要使用PreparedSatement对象解决SQL注入。
-- 在java语言中修复sql注入问题,通过占位符代替实际参数
SELECT * FROM USER WHERE username = ? AND password = ?
-- 帅哥模仿sql解决思想(大家不要去较真...)
SELECT * FROM USER WHERE username = "admin'#" AND PASSWORD = ""
PreparedSatement基础语法
// 1.获取连接
// 2.编写sql【占位符代替实际参数】
String sql = "SELECT * FROM USER WHERE username = ? AND password = ?";
// 3.获取sql预编译执行对象,先发送给数据库进行预编译
PreparedStatement pstmt = connection.prepareStatement(sql);
// 4.设置占位符实际参数
pstmt.setString(1,"admin'#");
pstmt.setString(2,"");
// 5.执行sql语句,并返回结果【注意,不需要传递sql参数】
ResultSet resultSet = pstmt.executeQuery();
// 6.处理结果
// 7.释放资源
优点
- 防止sql注入,提高安全性
- 参数与sql分离,提高代码可读性
- 减少编译次数,提高性能
1.2 操作
完善用户登录案例
修复day23_login的LoginServlet
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 统一编码
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 1.接收请求
String username = request.getParameter("username");
String password = request.getParameter("password");
try {
// 2.操作JDBC
// 2.1 获取连接
Connection connection = JdbcUitls.getConnection();
// 修复用户案例
// 2.2 编写sql语句
String sql = "SELECT * FROM USER WHERE username = ? AND password = ?";
System.out.println(sql);
// 2.3获取sql预编译对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 2.4 设置实际参数
preparedStatement.setString(1, username);
preparedStatement.setObject(2, password);
// 2.5执行sql语句
ResultSet resultSet = preparedStatement.executeQuery();
// 3.判断是否登录成功
if (resultSet.next()) {// 成功
String loginUsername = resultSet.getString("username");
request.getSession().setAttribute("loginUsername", loginUsername);
response.sendRedirect(request.getContextPath() + "/list.jsp");
} else {// 失败
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
// 4.释放资源
JdbcUitls.close(preparedStatement, connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
重写添加一条记录
// 新增
@Test
public void testInsert() throws Exception {
// 1.获取连接
Connection connection = JdbcUitls.getConnection();
// 2.编写sql
String sql = "insert into user values(null,?,?)";
// 3.获取sql预编译执行对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// --------------- 插入多个用户
// 4.设置实际参数
preparedStatement.setString(1, "jerry");
preparedStatement.setString(2, "999");
// 5.执行sql,并返回结果
int i = preparedStatement.executeUpdate();
// 6.处理结果
if (i > 0) {
System.out.println("第一条记录添加成功....");
}
// 在插入一个用户(了解....)
preparedStatement.setString(1, "xiaoming");
preparedStatement.setString(2, "999");
i = preparedStatement.executeUpdate();
if (i > 0) {
System.out.println("第二条记录添加成功....");
}
// -----------------
// 7.释放资源
JdbcUitls.close(preparedStatement, connection);
}
重写更新一条记录
// 修改
@Test
public void testUpdate() throws Exception {
// 1.获取连接
Connection connection = JdbcUitls.getConnection();
// 2.编写sql
String sql = "update user set username = ? where id = ?";
// 3.获取预编译sql执行对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 4.设置实际参数
preparedStatement.setString(1, "xiaoxiaoming");
preparedStatement.setInt(2, 8);
// 5.执行sql,并返回结果
int i = preparedStatement.executeUpdate();
// 6.处理结果
if(i>0){
System.out.println("修改成功.....");
}
// 7.释放资源
JdbcUitls.close(preparedStatement, connection);
}
重写删除一条记录
// 删除
@Test
public void testDelete() throws Exception {
// 1.获取连接
Connection connection = JdbcUitls.getConnection();
// 2.编写sql
String sql = "delete from user where id = ?";
// 3.获取sql预编译执行对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 4.设置实际参数
preparedStatement.setInt(1, 8);
// 5.执行sql并返回结果
int i = preparedStatement.executeUpdate();
// 6.处理结果
if (i > 0) {
System.out.println("删除成功....");
}
// 7.释放资源
JdbcUitls.close(preparedStatement, connection);
}
重写查询一条记录【课下作业...】
如果是查询所有的话,可以省略...(实际参数这一步)
二 连接池【了解】
2.1 概述
连接池其实就是一个容器(集合),存放数据库连接的容器。
当系统初始化好后,容器被创建,容器中会申请一些连接对象,当用户来访问数据库时,从容器中获取连接对象,用户访问完之后,会将连接对象归还给容器。
优点
- 节约资源,减轻服务器压力
- 提高连接复用性,用户访问高效
常见连接池
* DBCP:Apache提供的数据库连接池技术。
* C3P0:数据库连接池技术,目前使用它的开源项目有Hibernate、Spring等。
* HikariCP:小日本开发的连接池技术,性能之王,目前使用它的开源项目有SpringBoot等。
* Druid(德鲁伊):阿里巴巴提供的数据库连接池技术,是目前最好的数据库连接池。
600+项目中使用,支持sql日志监控
实现
Java为数据库连接池提供了公共的接口: DataSource ,各个连接池厂商去实现这套接口,提供jar包。
1. DataSource
功能
* 获取连接:
Connection getConnection()
* 归还连接:
connection.close()
如果连接对象是通过连接池获取的,那么执行 connection.close() 方法时,是归还到连接池,而不是销毁对象..
底层通过动态代理技术对close()方法进行了增强
2.2 Druid连接池
2.2.1 快速入门
① 导入druid相关jar包
② 编写测试代码【硬编码有耦合】
public class DruidDemo1 {
public static void main(String[] args) throws Exception {
// 1.创建druid连接池对象
DruidDataSource dataSource = new DruidDataSource();
// 2.与mysql建立连接(初始化一些连接个数),设置数据库基本四项
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/day23");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 3.从连接池中获取连接
Connection connection = dataSource.getConnection();
// 4.处理业务
System.out.println(connection);
// 5.归还连接
connection.close();// 注意:此方法执行完毕,不是销毁对象,而是归还到连接池容器中...
}
}
2.2.2 配置文件
① 定义配置文件
② 编写代码
public class DruidDemo2 {
public static void main(String[] args) throws Exception {
// 通过类加载器读取src目录下配置文件,获取io流
InputStream is = DruidDemo2.class.getClassLoader().getResourceAsStream("druid.properties");
// 创建 Properties对象
Properties properties = new Properties();
properties.load(is);
// druid连接池工厂对象,初始化连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 获取连接
for (int i = 1; i <= 11; i++) {
Connection connection = dataSource.getConnection();
System.out.println(connection);
if(i== 10){
connection.close();
}
}
// 归还连接
// connection.close();
}
}
2.3 连接池工具类【课下抄一遍...】
我们每次操作数据库都需要创建连接池,获取连接,关闭资源,都是重复的代码。我们可以将创建连接池和获取连接池的代码放到一个工具类中。
保证连接池对象,只创建一次
目的:简化书写,提高效率
public class JdbcUtils{
// 1.初始化连接池对象(druid),一个项目只有一个 static{}
// 2.提供获取连接池对象静态方法
// 3.提供连接对象的静态方法
// 4.提供释放资源的静态方法(connection是归还)
}
public class JdbcUtils {
private static DataSource dataSource = null;
// 1.初始化连接池对象(druid),一个项目只有一个 static{}
static {
try {
// 通过类加载器读取src目录下配置文件,获取io流
InputStream is = JdbcUtils.class.getClassLoader().getResourceAsStream("druid.properties");
// 创建 Properties对象
Properties properties = new Properties();
properties.load(is);
// druid连接池工厂对象,初始化连接池
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
// 2.提供获取连接池对象静态方法
public static DataSource getDataSource() {
return dataSource;
}
// 3.提供连接对象的静态方法
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 4.提供释放资源的静态方法(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();
}
}
}
// 重载关闭方法
public static void close(Statement statement, Connection connection) {
close(null, statement, connection);
}
}
三 案例:用户登录
需求
使用三层架构+JDBC连接池技术(面向接口编程),实现用户登录案例
我们在黑马旅游网课程中介绍面向接口编程....
3.1 需求分析
3.2 环境搭建
① 使用昨天的数据库和user表
② 创建web工程,导入相关jar包
③ 导入页面资源
④ 创建三层包目录结构
⑤ 导入JbdcUtils工具类
⑥ 编写User实体类
一个java实体(class类) 对应一张表
一个java对象对应一条记录
3.3 代码实现
① login.jsp
② LoginServlet
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 处理编码
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 1.接收请求参数
String username = request.getParameter("username");
String password = request.getParameter("password");
// 2.调用service
UserService userService = new UserServiceImpl();
User user = userService.login(username, password);
// 3.判断
if (user == null) {
// 登录失败,友情提示
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
} else {
// 登录成功
request.getSession().setAttribute("user", user);
response.sendRedirect(request.getContextPath() + "/list.jsp");
}
}
}
③ UserService 接口
public interface UserService {
// 我来定义规范(根据用户名和密码查询user对象)
public User login(String username, String password);
}
④ UserServiceImpl 实现类
public class UserServiceImpl implements UserService {
@Override
public User login(String username, String password) {
UserDao userDao = new UserDaoImpl();
return userDao.login(username, password);
}
}
⑤ UserDao 接口
public interface UserDao {
// 我来定义规范(根据用户名和密码查询user对象)
public User login(String username, String password);
}
⑥ UserDaoImpl 实现类
public class UserDaoImpl implements UserDao {
@Override
public User login(String username, String password) {
Connection connection =null;
PreparedStatement preparedStatement = null;
ResultSet resultSet =null;
try {
// 1.获取连接【从连接池】
connection = JdbcUtils.getConnection();
// 2.编写sql
String sql = "select * from user where username = ? and password = ?";
// 3.获取sql预编译执行对象
preparedStatement = connection.prepareStatement(sql);
// 4.设置实际参数
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);
// 5.执行sql并返回结果
resultSet = preparedStatement.executeQuery();
// 6.处理结果
User user = null;
if (resultSet.next()) {
// 获取 id 用户名、密码
int id = resultSet.getInt("id");
username = resultSet.getString("username");
password = resultSet.getString("password");
user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
}
return user;
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 7.释放资源
JdbcUtils.close(resultSet, preparedStatement, connection);
}
return null;
}
}
⑦ list.jsp
老师晚上总结
疑惑|拓展
-
PrePareStatement的好处
- 防止sql注入
- sql的参数与sql语句分离,增强阅读性
- 如果一个sql语句执行多次的时候会提高效率
-
PrePareStatement执行的流程与常见的异常
package com.itheima.test; import com.itheima.utils.JdbcUitls; import org.junit.Test; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class Demo1 { @Test public void test01() throws SQLException { //1. 获取链接 Connection connection = JdbcUitls.getConnection(); //2. 准备sql语句,得到PreparedStatement String sql = "insert into account(name,balance) values(?,?)"; PreparedStatement pst = connection.prepareStatement(sql); //3. 设置参数,需要根据列的设置参数,这样子就太繁琐了 pst.setObject(1,"罗志祥"); pst.setObject(2,1000); //4. 执行sql pst.executeUpdate(); //5. 关闭链接 pst.close(); connection.close(); } }
==常见的异常:实参的个数与?个数不匹配,或者是索引用错==
-
为什么需要使用连接池,连接池的工作原理
- 使用连接池的原因: jdbc的操作中获取连接是比较耗时的,如果每次用户访问的时候采取获取连接,那么会导致用户体验不佳,因为用户访问访问需要等待问数据库获取连接。
==今晚简单的和大家自定义一个连接池,简单版的,只是为了让大家看连接池更加透彻==
package com.itheima.utils; import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.LinkedList; import java.util.logging.Logger; //自定义的一个连接池 /* 连接池sun公司要求一定要实现DataSource接口 */ public class MyDataSource implements DataSource { //定义一个集合保存链接 private LinkedList
list = new LinkedList<>(); //初始化的链接个数 private int initSize = 5; //一旦创建连接池就要有五个链接在连接池中 private int maxSize = 10; //连接池最大的链接个数 private int curConnectionCount=0; //定义一个变量记录当前已经创建了几个链接 //一旦创建连接池,那么连接池就必须有5个链接。 public MyDataSource() { for (int i = 0; i 0){ //取出链接(本质要从集合中删除) Connection connection = list.pop(); //出栈,删除并返回 return connection; } //情况2: 连接池中没有链接,但是没有超过最大链接个数,还可以创建新的链接 if(curConnectionCount T unwrap(Class iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class> iface) throws SQLException { return false; } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { } @Override public void setLoginTimeout(int seconds) throws SQLException { } @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } } 测试代码
@Test public void test02() throws SQLException { MyDataSource dataSource = new MyDataSource(); for (int i = 0; i <10 ; i++) { Connection connection = dataSource.getConnection(); System.out.println(i+" : "+ connection); if(i==9){ //把第10个用完放回连接池中 dataSource.close(connection); } } //第11个 System.out.println(dataSource.getConnection()); }
- DRUID参数的说明
创建了100个连接, 深夜的时候只有3个人在使用,那么空闲有97个连接, 连接需要关闭空闲连接,剩下20个人
minIdle : 最小的空闲个数
传智播客刚刚创立只有2位老师, 随着公司发展传智老板说最多公司人数可以达到1w人, 2020年
由于疫情开班没有那么多,有些老师空闲出来了, 那么我空闲的老师最大容量是1000人。
传智播客: 10000员工
在授课的老师: 8000人
空闲的老师有:2000人 , 裁员1000人 , 最多空闲1000人
今日重点
- PreparedStatement实现增删查改
- 连接池
答疑区
疑问 | 答复 |
---|---|
resultset结果集需要先封装成对象或者json再返回还是直接返回再封装? | 一般我们都是先封装,再返回 |
预编译和即时编译有什么区别? | String sql = "select name,balance from account"; rs = st.executeQuery(sql); //即时编译,是在执行sql的时候发送给mysql服务器 String sql2 = "select name,balance from account"; PreparedStatement pst = connection.prepareStatement(sql2);//执行sql语句之前就发送给mysql服务器,该语句就发送给服务器了 |
以后全部用P reparedstatement 再也不使用statement | 是的 |
为什么说pstmt.excuteQuery()进行了子接口方法的重载 | PreparedStatement 是 Statement的子接口, 也拥有executeQuery(String sql)方法, 但是一般不用, 因为它预编译sql,并重载了executeQuery(无参)的方法 |
如果,占位符 有三个 我传入两个值 ,这样能执行吗 | 执行报错,实参与形参个数不对应 |
我的pstmt.setString(1, 1);传入参数,数据库接收到参数后就不编译了吗? 我传入参数后到数据库拿到参数后不是又执行一次吗? 这个数据库编译的概念是什么?是不是编译后就和java一样有个编译文件,而后我们把参数传入到编译后的文件中? | 编译就是有点相当于把sql语句编译成一个类似函数的东西,传参执行sql,就相当于调用了函数 |
我如果把每个问号设置两次,他会覆盖还是新的一行sql代码,如果是新的sql代码执行的时候是只执行一条还是执行两条,还是怎么的执行? | 连续设置两次会覆盖原来赋予的值。 |
我们一般对一次crud添加事务还是对多次? | 执行单条sql语句不需要添加事务。 |
老师,可不可以理解成每一个connect就是一个事务,如果取消自动登录的话 | 每个Connection都可以控制开启或者不开启事务。但是你没有开启事务就没有事务 |
不对吧,不是单个sql语句不也是事务吗?conn.setAutoCommit(false)是关闭自动提交事务,如果不关默认自动提交事务,那么我们一个Connection连接就是一个事务,conn.setAutoCommit(false)不是开启事务的按钮,他是控制是否自动提交的按钮。 | conn.setAutoCommint(false) 只是设置不自动提交事务,指定多条sql语句,需要设置。执行单条sql'语句不需要设置,默认的是自动提交事务。 |
一个Connection连接里面的全部sql语句就是一个事务,conn.setAutoCommit(false)只是设置自动提交的方法 | 如果一个Connection对象,不设置conn.setAutoCommit(false),那么每执行一个sql语句都是自动提交,那么就会存在多个事务。 所以如果需要保证多个sql语句执行是同一个事务的话,那么我们必须设置conn.setAutoCommit(false) |
设置成员变量有什么好处 | 由于多个方法都需要使用DataSource. |
定义一个接口的好处是什么呢 | 用到了Srping之后感受会比较深,现在暂时先写习惯。 后期会有详细的讲解与体验的 |
login方法的返回值用Boolean是不是不严谨?? | 主要我们还需要把登陆成功的用户存储在session中,所以返回一个User对象,当然实现一个功能是有多种方式,没有哪种对错,只要功能实现了就好 |
抽象类扩张性好是吗 | 抽象类和接口可以用于实现多态, 扩展性好 |
直接返回一个resultset对象集给web层行吗?在web层里封装到user对象行吗 | 不方便,因为dao层查询完毕之后需要关闭连接,一旦关闭连接resultSet不能再遍历 |
getInt("id"),id带“”不太清楚 | getXXX(String str) 参数是String类型, java中字符串要加 "" |
老师写的getInt("id")得到的id不太明白 | getId(String name) 形参要求是列名字符串。 |
改为手动添加事务直接添加吗?为什么我昨天直接添加了功能就实现不了了 | 如果是个人练习代码出现bug,那么可以找你们班的答疑老师帮助你调试,因为需要结合你的代码才能分析出具体的原因 |
是把数据库以字符串定义的列名id查询出来转成了intl类型是吗 | 是的 |