JDBC
JDBC概述
-
服务器和数据库连接的桥梁
-
数据库驱动:数据库厂商提供的用来操作数据库的jar包
-
JDBC简介
由于各大数据库厂商提供的数据库驱动各不相同, 导致了开发人员的学习成本十分的高,SUN公司为了简化数据库的操作, 提供了一套规范,本质上就是一大堆的接口,要求各大数据库厂商在提供驱动时都要实现JDBC这套接口,实现之后,只要学会JDBC这套接口,所有的数据库驱动就都会使用了
-
JDBC由两个包组成, 分别是
java.sql
和javax.sql
, 目前已经被集成到javase规范中 -
需要注意的是: JDBC中包含的就是一段接口, 真实操作数据库的代码都在具体的数据库驱动中,也就是说在开发数据库程序时, 除了要导入JDBC相关的包之外, 还需要导入具体的数据库驱动包.
六个步骤实现JDBC程序
- 注册数据库驱动:
- 获取数据库连接
- 获取传输器
- 利用传输器发送sql,返回一个结果集
- 处理结果:
next()
— 结果集的指针每次指向下一条数据,如果有则返回true,否则返回false - 释放资源
public class JDBCDemo2 {
public static void main(String[] args) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
// 1. 注册数据库驱动
Class.forName("com.mysql.jdbc.Driver"); // 反射
// 2. 获取连接
conn = DriverManager.getConnection(
"jdbc:mysql:///mydb1","root","root");
// 3. 创建传输器
stat = conn.createStatement();
// 4. 利用传输器发送sql,接收返回值
rs= stat.executeQuery("SELECT * FROM exam");
// 5. 处理结果
// rs.next() 结果集的指针每次指向下一条数据,如果有则返回true,否则返回false
while (rs.next()){
System.out.println(rs.getInt("id")+","
+rs.getString("name") + ","
+ rs.getDouble("math"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 释放资源
if (conn != null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
conn = null;
}
}
if (stat != null){
try {
stat.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
stat = null;
}
}
if (rs != null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
rs = null;
}
}
}
}
}
JDBC API
注册数据库驱动:
-
DriverManager.registerDriver(new Driver())
使用DriverManager.registerDriver(new Driver()),注册数据库有两个缺点,首先,通过观察mysql的中Driver接口的实现类发现在静态代码块中注册驱动的逻辑,所以这种方式会造成驱动被注册两次。另外,这种方式导致了程序和具体的数据库驱动绑死在了一起,程序的灵活性比较低。
-
Class.forName("com.mysql.jdbc.Driver")
推荐使用这样的方式注册数据库驱动。
获取数据库连接
-
Connection conn = DriverManager.getConnection(url,name,psw);
参数1 — url, 参数二 — 用户名,参数三 — 密码
数据库URL
URL用于标识数据库的位置,程序员通过URL地址告诉JDBC程序连接哪个数据库
常用数据库URL地址的写法:
-
Oracle写法:
jdbc:oracle:thin:@localhost:1521:dbName
-
SqlServer写法:
jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=dbName
-
MySql写法:
jdbc:mysql://localhost:3306/dbName
-
Mysql的url地址的简写形式:
jdbc:mysql:///dbName
如果端口号是3306可以省略,如果ip为localhost也可以省略
Connection
Jdbc程序中的Connection,它用于代表数据库的链接
Connection是数据库编程中最重要的一个对象,客户端与数据库所有交互都是通过connection对象完成的,这个对象的常用方法:
-
createStatement()
:创建向数据库发送sql的statement对象。 -
prepareStatement(sql)
: 创建向数据库发送预编译sql的PreparedSatement
对象。 -
prepareCall(sql)
: 创建执行存储过程的callableStatement对象。 -
setAutoCommit(boolean autoCommit)
:设置事务是否自动提交。 -
commit()
: 在链接上提交事务。 -
rollback()
: 在此链接上回滚事务。
Statement
Jdbc程序中的Statement对象用于向数据库发送SQL语句
Statement对象常用方法:
-
executeQuery(String sql)
:用于向数据库发送查询语句。 -
executeUpdate(String sql)
:用于向数据库发送insert、update或delete语句 -
execute(String sql)
:用于向数据库发送任意sql语句 -
addBatch(String sql)
:把多条sql语句放到一个批处理中。 -
executeBatch()
:向数据库发送一批sql语句执行。
ResultSet
Jdbc程序中的ResultSet用于代表Sql语句的执行结果。
Resultset封装执行结果时,采用的类似于表格的方式。
ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。
ResultSet对象是用于封装执行结果的,该对象提供的都是用于获取数据的get方法:
- 获取任意类型的数据
getObject(int index)
: 根据列的下标获取
getObject(string columnName)
: 根据列名获取
- 获取指定类型的数据,如:
getString(int index)
& getString(String columnName)
getInt(columnIndex)
& getInt(columnLabel)
getDouble(columnIndex)
& getDouble(columnLabel)
注意:最好使用列名获取数据
- 操作游标的方法,如:
next()
:移动到下一行
Previous()
:移动到前一行
absolute(int row)
:移动到指定行
beforeFirst()
:移动resultSet的最前面。
afterLast()
:移动到resultSet的最后面。
释放资源
-
Jdbc程序运行完后,切记要释放程序在运行过程中创建的与数据库进行交互的对象,包括ResultSet, Statement和Connection对象。
-
Connection对象,是非常稀有的资源,用完后必须马上释放,如果Connection不能及时、正确的关闭,极易导致系统宕机。
-
Connection的使用原则是尽量晚创建,尽量早的释放。
-
为确保资源释放代码能运行,资源释放代码也一定要放在finally语句中。
JDBC的增删改查
-
编写配置文件 jdbcConfig.properties
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSN732xV-1641389309398)(D:\javaweb\Day07\pic\捕获.PNG)]
-
编写工具类JavaUtils
- 通过JavaUtils读取配置文件(相对路径读取),并通过配置参数获取数据库连接
- 通过JavaUtils释放资源
public class JDBCUtils { // 释放资源 public static void close(Connection conn, Statement stat, ResultSet rs){ if (conn != null){ try { conn.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { conn = null; } } if (stat != null){ try { stat.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { stat = null; } } if (rs != null){ try { rs.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { rs = null; } } } // 读取配置文件 private static Properties pro = new Properties(); static { try { // 路径不能写死 // 可以根据当前类找到编译的class的根目录 // 获取类加载器,类加载器可以直接进入类的根目录 String path = JDBCUtils.class.getClassLoader(). getResource("jdbcConfig.properties").getPath(); pro.load(new FileInputStream(path)); } catch (IOException e) { e.printStackTrace(); } } // 获取连接 public static Connection getConn(){ try { Class.forName(pro.getProperty("driverClass")); Connection conn = DriverManager.getConnection( pro.getProperty("jdbcUrl"),pro.getProperty("user"), pro.getProperty("password")); return conn; } catch (Exception e) { e.printStackTrace(); return null; } } }
-
增
public class JDBCDelete { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { conn = JDBCUtils.getConn(); stat = conn.createStatement(); String sql = "insert into exam values(null,'lili',90,80,95);"; stat.executeUpdate(sql); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.close(conn,stat,rs); } } }
-
删
public class JDBCDelete { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { conn = JDBCUtils.getConn(); stat = conn.createStatement(); String sql = "DELETE FROM exam WHERE id = 5"; stat.executeUpdate(sql); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.close(conn,stat,rs); } } }
-
改
public class JDBCUpdate { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { conn = JDBCUtils.getConn(); stat = conn.createStatement(); String sql = "Update exam SET name = '朱元璋' WHERE id = 6"; stat.executeUpdate(sql); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.close(conn,stat,rs); } } }
-
查
public class JDBCSearch { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { conn = JDBCUtils.getConn(); stat = conn.createStatement(); String sql = "SELECT * FROM exam"; ResultSet rs = stat.executeQuery(sql); while(rs.next()){ System.out.println(rs.getInt("id")+"," +rs.getString("name") + "," + rs.getDouble("math")); } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.close(conn,stat,rs); } } }
PreparedStatement
注入攻击演示
由于后台的SQL是拼接而来的, 其中的参数是用户提交的, 如果用户在提交参数时,参杂了一些SQL关键字或者特殊符号,就有可能会导致SQL语句语意的改变,从而造成一些意外的操作
注入攻击示例:
public class Login {
// 注入攻击演示
public static void main(String[] args) {
String username = "zss'#'";
String password = "1111";
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConn();
stat = conn.createStatement();
String sql = "SELECT * FROM user WHERE name = '"+ username+"' and password = '" +password +"'";
rs = stat.executeQuery(sql);
if (rs.next()){ // 有数据
System.out.println("恭喜登陆成功");
}
else{
System.out.println("用户名或者密码错误");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,stat,rs);
}
}
}
-
结果为恭喜登陆成功
-
在密码错误的情况下,通过拼接 # 注释验证密码的sql语句,注册成功
PreparedStatement
PreparedStatement优点:
-
可以防止sql注入攻击
通过PreparedStatement对象发送sql,先把sql语句的骨架发送给数据库编译并确定下来,后面发送的只能是参数的值,不能影响sql语句的骨架,即使参数中包含sql关键字或特殊符号,也只会当成普通的文本来处理
-
通过方法来设置参数,省去了拼接SQL语句的麻烦
-
可以提高程序的效率
PreparedStatement对象发送的sql语句(骨架)到数据库编译后会被数据缓存下来, 如果下次执行的sql与缓存中的相匹配,就不再编译而是直接使用缓存中的语句,可以减少sql语句编译的次数, 提高程序执行的效率
Statement对象发送的sql语句到数据库之后也会编译,但是Statement对象是先拼接好再发送sql到数据库,如果每次参数不同,整条sql也就不同,所以每次都需要编译
对上述代码的修改:
public class Login {
public static void main(String[] args) {
String username = "zss'#'";
String password = "111";
Connection conn = null;
// Statement stat = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConn();
ps = conn.prepareStatement("SELECT * FROM user WHERE name = ? and password = ?");
ps.setString(1,username);
ps.setString(2,password);
rs = ps.executeQuery();
if (rs.next()){ // 有数据
System.out.println("恭喜登陆成功");
}
else{
System.out.println("用户名或者密码错误");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,ps,rs);
}
}
}
批处理
批处理概述
假设现有一大堆的sql要到数据库执行,如果一条一条发送,有多少条就 需要发送多少次,效率低下
可以通过批处理提高发送sql语句的效率: 将这一大堆的sql添加到 一个批中,一次性将批发送给数据库,数据库收到后打开批,依次执行其中 sql语句,这样可以减少sql语句发送的次数,从而提高程序执行的效率
Statement方式来实现批处理
优点:
- 可以在一次批处理中添加结构不同的sql语句
缺点:
- 不能防止sql注入攻击
- 没有预编译机制,效率略低
- 当发送结构相同的sql语句时,sql语句的骨架每次都需要编写。
public class StatBatch {
public static void main(String[] args) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql:///mydb1","root","root");
stat = conn.createStatement();
// 将 sql 添加到批中
stat.addBatch("use mydb1");
stat.addBatch("create table tb1( id int, name varchar(20), password varchar(20))");
stat.addBatch("insert into tb1 values(1,'root','root')");
stat.addBatch("insert into tb1 values(2,'zss','zss')");
stat.addBatch("insert into tb1 values(3,'lili','lili')");
// 执行批
stat.executeBatch();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,stat,rs);
}
}
}
PreparedStatement方式实现批处理
优点:
-
可以防止sql注入攻击
-
采用预编译机制,可以提高程序执行的效率
-
当发送多条结构相同的sql时,sql语句的骨架可以只发送一次。
缺点:
- 不能在一次批处理中添加结构不同的sql语句
public class PsBatch {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql:///mydb1","root","root");
ps = conn.prepareStatement("insert into tb1 values(?,?,?)");
// 将 sql 添加到批中
// 可以通过map遍历添加
ps.setInt(1,4);
ps.setInt(1,5);
ps.setString(2,"Sam");
ps.setString(2,"Tom");
ps.setString(3,"Sam");
ps.setString(3,"Tom");
ps.addBatch();
// 执行批
ps.executeBatch();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,ps,rs);
}
}
}
连接池
连接池概述
用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机
频繁的开关连接相当的耗费资源,所以我们可以设置一个连接池,在程序启动时就初始化一批连接,在程序中共享,需要连接时从池中获取,用完再还回池中,通过池共享连接,减少开关连接的次数,提高程序的效率。
自定义连接池
自定义连接池,需要在使用完连接后记得不能关闭连接,而是要调用returnConn
方法将连接还回池中。
-
自定义连接池
public class MyPool implements DataSource { // 1. 准备存放连接的容器 private static List<Connection> list= new LinkedList<Connection>(); // 2. 在静态代码块中初始化一批连接,放入容器 static{ try { Class.forName("com.mysql.jdbc.Driver"); for (int i = 0; i <5; i++){ Connection conn = DriverManager.getConnection( "jdbc:mysql:///mydb1","root","root" ); list.add(conn); } } catch (Exception e) { e.printStackTrace(); } } // 3. 重写getConnection(),用来获取连接 @Override public Connection getConnection() throws SQLException { // 3.1. 判断容器中是否为空,如果为空,创建n条连接 if(list.isEmpty()){ for (int i = 0; i <5; i++){ Connection conn = DriverManager.getConnection( "jdbc:mysql:///mydb1","root","root" ); list.add(conn); } } // 3.2. 从容器中获取连接,并将此连接删除 System.out.println("从连接池获取一条连接,连接池还剩"+list.size()+"条连接"); return list.remove(0); } // 4. 自定义还连接的方法 public void returnConn(Connection conn){ // 4.1 判断连接是否可用 (是否是null和close) // 4.2 若可用,则归还连接 try { if (conn != null && ! conn.isClosed()){ list.add(conn); System.out.println("收到还回的连接,连接池还剩"+list.size()+"条连接"); } } catch (SQLException throwables) { throwables.printStackTrace(); } } // ... 省略其他重写方法 }
-
测试连接池
public class MyPoolTest { public static void main(String[] args) { Connection conn = null; Statement stat = null; MyPool pool = new MyPool(); try { conn = pool.getConnection(); stat = conn.createStatement(); String sql = "DELETE FROM exam WHERE id = 5"; stat.executeUpdate(sql); } catch (Exception e) { e.printStackTrace(); } finally { if (stat != null){ try { stat.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { stat = null; } } pool.returnConn(conn); // 归还连接 } } }
close方法的改造
-
继承
写一个类继承要改造的类,对于不想改造的方法不覆盖,对于想要改造的方法复写该方法,将代码改造为自己需要的逻辑代码。
这种方式只能在还没有对象的情况下使用,现在Connection对象已经存在了,再用继承复写的方式是不行的,不适用。
-
装饰
实现装饰设计模式:
- 写一个装饰类,要求装饰类和被装饰者所属的类实现同一个接口或者继承同一个父类
- 装饰类必须提供构造方法接收被装饰者,并将被装饰者保存在类的内部
- 对于想要改造的方法直接进行改造,对于不想改造的方法,直接调用原有对象(被装设者)上的方法
代码实现:
- 创建装饰类DecorateConnection,实现Connection接口
- 通过构造方法接收被装饰的类并保存
- 对于不想改造的方法,调用原方法(生成 —> 委托方法 (DelegateMethod) — > 选中不想改造的方法 ),希望改造的自行改造
装饰类:
public class DecorateConnection implements Connection {
// 将被装饰的类通过构造方法接受并保存
private Connection conn;
private MyPool pool;
public DecorateConnection(Connection conn, MyPool pool){
this.conn = conn;
this.pool = pool;
}
// 不想改造的方法直接调用原有方法,希望改造的自行改造
// 生成 --- > 委托方法 (D)
@Override
public void close() throws SQLException {
pool.returnConn(conn);
}
// ... 省略自动生成的原有方法
}
MyPool类
public class MyPool implements DataSource {
// 1. 准备存放连接的容器
private static List<Connection> list= new LinkedList<Connection>();
// 2. 在静态代码块中初始化一批连接,放入容器
static{
try {
Class.forName("com.mysql.jdbc.Driver");
for (int i = 0; i <5; i++){
Connection conn = DriverManager.getConnection(
"jdbc:mysql:///mydb1","root","root"
);
list.add(conn);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 3. 重写getConnection(),用来获取连接
@Override
public Connection getConnection() throws SQLException {
// 3.1. 判断容器中是否为空,如果为空,创建n条连接
if(list.isEmpty()){
for (int i = 0; i <5; i++){
Connection conn = DriverManager.getConnection(
"jdbc:mysql:///mydb1","root","root"
);
list.add(conn);
}
}
// 3.2. 从容器中获取连接,并将此连接删除
Connection conn = list.remove(0);
DecorateConnection dcoon = new DecorateConnection(conn,this);
return dcoon;
}
// 4. 自定义还连接的方法
// ... 不变
// ... 省略其他重写方法
}
测试类: 直接调用close()
开源数据库连接池c3p0
- 导入c3p0的jar包
- 创建配置文件
- 创建连接池
public class Testc3p0 {
public static void main(String[] args) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
MyPool pool = new MyPool();
try {
// 创建连接池
ComboPooledDataSource cpds = new ComboPooledDataSource();
//设置参数
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql:///mydb1");
cpds.setUser("root");
cpds.setPassword("root");
conn = cpds.getConnection();
stat = conn.createStatement();
String sql = "DELETE FROM tb1 WHERE id = 5";
stat.executeUpdate(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,stat,rs);
}
}
}
c3p0提供了两种默认的配置文件: c3p0-config.xml \ c3p0.properties
c3p0.properties:
c3p0.driverclass = com.mysql.jdbc.Driver
c3p0.jdbcUrl = jdbc:mysql:///mydb1
c3p0.user = root
c3p0.password = root
编写配置文件后,无需设置参数,底层自动读取配置文件中的参数
public class Testc3p0 {
public static void main(String[] args) {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
MyPool pool = new MyPool();
try {
// 创建连接池
ComboPooledDataSource cpds = new ComboPooledDataSource();
conn = cpds.getConnection();
stat = conn.createStatement();
String sql = "DELETE FROM tb1 WHERE id = 4";
stat.executeUpdate(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.close(conn,stat,rs);
}
}
}