目录
JDBC简介
Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。
数据库驱动
什么是数据库驱动?类似声卡驱动、显卡驱动
我们的程序会通过数据库驱动,和数据库打交道。
JDBC
SUN 公司为了简化开发人员的(对数据库的统一)操作,提供了一个(Java操作数据库的)规范,JDBC。
这些规范的实现由具体的厂商去做。
对于开发人员来说,我们只需要掌握JDBC的接口操作即可。
java.sql
javax.sql
还需要导入数据库驱动包
第一个JDBC程序
1.创建测试数据库
CREATE DATABASE jdbcStudy CHARACTER SET utf8 COLLATE utf8_general_ci;
USE jdbcStudy;
CREATE TABLE `users`(
id INT PRIMARY KEY,
NAME VARCHAR(40),
PASSWORD VARCHAR(40),
email VARCHAR(60),
birthday DATE
);
INSERT INTO `users`(id,NAME,PASSWORD,email,birthday)
VALUES(1,'zhansan','123456','zs@sina.com','1980-12-04'),
(2,'lisi','123456','lisi@sina.com','1981-12-04'),
(3,'wangwu','123456','wangwu@sina.com','1979-12-04')
2.创建一个普通项目
3.导入数据库驱动
pom.xml
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
4.编写测试代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class JdbcDemo {
public static void main(String[] args) throws Exception {
//1. 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");//固定写法 jdbc驱动版本8.0之前为com.mysql.jdbc.Driver
//2. 用户信息和url
//useUnicode=true&characterEncoding=utf8&&useSSL=true
//useUnicode=true支持中文编码;characterEncoding=utf8中文字符集设置为utf-8;useSSL=true使用安全链接
//jdbc驱动版本8.0之后要添加serverTimezone=UTC
String url ="jdbc:mysql://localhost:3306/jdbcstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&&useSSL=true";
String name = "root";
String password = "123456";
//3. 连接成功,返回数据库对象 connection代表数据库
Connection connection= DriverManager.getConnection(url,name,password);
//4. 执行SQL的对象 statement 执行SQL的对象
Statement statement = connection.createStatement();
//5. 执行SQL的对象 去执行SQL 可能存在结果,查看返回结果
String sql="SELECT * FROM users";
ResultSet resultSet = statement.executeQuery(sql);//返回的结果集,结果集中封装了我们全部查询的结果
while(resultSet.next()){
System.out.println("ID="+resultSet.getObject("id"));
System.out.println("姓名="+resultSet.getObject("NAME"));
System.out.println("密码="+resultSet.getObject("PASSWORD"));
System.out.println("邮箱="+resultSet.getObject("email"));
System.out.println("生日="+resultSet.getObject("birthday"));
System.out.println("==================================");
}
//6. 释放连接(从后往前释放)
resultSet.close();
statement.close();
connection.close();
}
}
JDBC中各对象详解
步骤总结:
1.加载驱动
2.连接数据库 DriverManager
3.获取执行SQL的对象 Statement
4.获得返回的结果集
5.释放连接
DriverManager
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());
Class.forName("com.mysql.jdbc.Driver");//固定写法
Connection connection= DriverManager.getConnection(url,name,password);
//connection代表数据库
//数据库设置自动提交
//事务提交
//事务回滚
connection.rollback();
connection.commit();
connection.setAutoCommit();
URL
String url ="jdbc:mysql://localhost:3306/jdbcstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&&useSSL=true";
//mysql 默认端口3306
//协议://主机地址:端口号/数据库名?参数1&参数2&参数3...
//Oracle 1521
//jdbc:oralce:thin:@localhost:1521:sid
statement执行SQL的对象 PrepareStatement 执行SQL的对象
String sql="SELECT * FROM users";//编写Sql
statement.executeQuery();//查询操作,返回ResultSet
statement.execute();//执行任何SQL
statement.executeUpdate();//更新,插入,删除,返回一个受影响的行数
ResultSet 查询的结果集,封装了所有的查询结果
获得指定的数据类型
ResultSet resultSet = statement.executeQuery(sql);//返回的结果集,结果集中封装了我们全部查询的结果
resultSet.getObject();//在不知道列类型下使用
//如果知道则使用指定类型
resultSet.getString();
resultSet.getInt();
resultSet.getFloat();
resultSet.getDate();
...
遍历,指针
resultSet.next(); //移动到下一个
resultSet.afterLast();//移动到最后
resultSet.beforeFirst();//移动到最前面
resultSet.previous();//移动到前一个
resultSet.absolute(row);//移动到指定行
释放内存
//6. 释放连接(从后往前释放)
resultSet.close();
statement.close();
connection.close();//connection连接最占用资源
statement对象
JDBC中的statement对象用于向数据库发送SQL语句,想完成对数据库的增删改查,只需要通过这个对象向数据库发送增删改查语句即可。
Statement对象的executeUpdate方法,用于向数据库发送增、删、改的sq|语句, executeUpdate执行完后, 将会返回一个整数(即增删改语句导致了数据库几行数据发生了变化)。
Statement.executeQuery方法用于向数据库发生查询语句,executeQuery方法返回代表查询结果的ResultSet对象。
CRUD操作-create
使用executeUpdate(String sql)方法完成数据添加操作,示例操作:
Statement statement = connection.createStatement();
String sql = "insert into user(...) values(...)";
int num = statement.executeUpdate(sql);
if(num>0){
System.out.println("插入成功");
}
CRUD操作-delete
使用executeUpdate(String sql)方法完成数据删除操作,示例操作:
Statement statement = connection.createStatement();
String sql = "delete from user where id =1";
int num = statement.executeUpdate(sql);
if(num>0){
System.out.println("删除成功");
}
CURD操作-update
使用executeUpdate(String sql)方法完成数据修改操作,示例操作:
Statement statement = connection.createStatement();
String sql = "update user set name ='' where name = ''";
int num = statement.executeUpdate(sql);
if(num>0){
System.out.println("修改成功");
}
CURD操作-read
使用executeUpdate(String sql)方法完成数据查询操作,示例操作:
Statement statement = connection.createStatement();
String sql = "select * from user where id =1";
ResultSet rs= statement.executeQuery(sql);
if(rs.next()){
System.out.println("");
}
包装成工具类
首先配置文件db.properties写好信息
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&&useSSL=true
username=root
password=123456
工具类
import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;
public class JdbcUtils {
private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
static {
try{
FileInputStream in = new FileInputStream("src/main/resources/db.properties");
Properties properties = new Properties();
properties.load(in);
driver=properties.getProperty("driver");
url=properties.getProperty("url");
username=properties.getProperty("username");
password=properties.getProperty("password");
//1.驱动只用加载一次
Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
}
}
//2.获取连接
public static Connection getConnection() throws Exception{
return DriverManager.getConnection(url, username, password);
}
//3.释放资源
public static void release(Connection conn, Statement st, ResultSet rs) throws SQLException {
if (rs != null) {
rs.close();
}
if (st != null){
st.close();
}
if(conn != null){
conn.close();
}
}
}
使用工具类(插入数据为例)(exectueUpdate
可以进行增删改三个操作)
import com.cheng.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import static com.cheng.utils.JdbcUtils.getConnection;
public class TestInsert {
public static void main(String[] args){
Connection conn =null;
Statement st = null;
ResultSet rs =null;
try {
conn = getConnection();//获取连接
st = conn.createStatement();//获取SQL执行对象
String sql = "INSERT INTO users(id,`NAME`,`PASSWORD`,`email`,`birthday`)" +
"VALUES(5,'你好','123456','233223@qq.com','2020-01-01')";
int i = st.executeUpdate(sql);
if(i>0){
System.out.println("插入成功");
}
JdbcUtils.release(conn,st,rs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用工具类(查询数据为例)
import com.cheng.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static com.cheng.utils.JdbcUtils.getConnection;
public class TestQuery {
public static void main(String[] args) throws SQLException {
Connection conn =null;
Statement st = null;
ResultSet rs =null;
try {
conn = getConnection();//获取连接
st = conn.createStatement();//获取SQL执行对象
String sql = "select * from users";
rs=st.executeQuery(sql);//查询完毕返回结果集
while (rs.next()){
System.out.println(rs.getString("NAME"));
}
JdbcUtils.release(conn,st,rs);
} catch (Exception e) {
e.printStackTrace();
}finally {
JdbcUtils.release(conn,st,rs);
}
}
}
SQL注入问题
sql存在漏洞,会被攻击导致数据泄露 SQL会被拼接 or
原理,例如密码查询正常是要匹配密码,但是加个or 1=1后就会认为密码查询通过,因为or是左右两边有一个成立都认为是成立,又因为1=1恒成立,所以服务器被欺骗,认为密码查询通过
示例
import com.cheng.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import static com.cheng.utils.JdbcUtils.getConnection;
public class SqlInjection {
public static void main(String[] args) {
// 正常查询
login("你好","123456");
System.out.println("=================");
// SQL注入
login("' OR '1=1","' OR '1=1");
}
public static void login(String name,String password){
Connection conn =null;
Statement st = null;
ResultSet rs =null;
try {
conn = getConnection();//获取连接
st = conn.createStatement();//获取SQL执行对象
// 正常查询:SELECT * FROM users WHERE `NAME`='你好' AND `PASSWORD`='123456'
// SQL注入:SELECT * FROM users WHERE `NAME`='' OR '1=1' AND `PASSWORD`='' OR '1=1'
// 即为SELECT * FROM users 这样就能获取整个表所有信息
String sql = "SELECT * FROM users WHERE `NAME`='"+ name +"' AND `PASSWORD`='"+ password +"'" ;
rs=st.executeQuery(sql);//查询完毕返回结果集
while (rs.next()){
System.out.println(rs.getString("NAME"));
}
JdbcUtils.release(conn,st,rs);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
JdbcUtils.release(conn,st,rs);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
PreparedStatement对象
PreparedStatement 可以防止SQL注入 ,效率更高。
public class Test {
public static void main(String[] args) {
Connection connection= null;
PreparedStatement pstm=null;
try {
connection = JdbcUtils.getConnection();
//PreparedStatement防止SQL注入本质:把传递进来的参数当作字符
//假设其中存在转义字符,比如说引号,会被直接转义
// 区别:
// 1.使用问号占位符代替参数
String sql = "insert into users(id, `NAME`, `PASSWORD`, `email`,`birthday`) values(?, ?, ?, ?, ?)";
// 2.预编译sql,先写sql然后不执行
pstm = connection.prepareStatement(sql);
// 手动赋值
pstm.setInt(1,4);// 1代表第一个问号
pstm.setString(2,"张三");
pstm.setString(3,"123123");
pstm.setString(4,"123333@qq.com");
pstm.setDate(5,new java.sql.Date(new Date().getTime()));// 注意要转换成sql的Date
//执行
int i = pstm.executeUpdate();
if (i>0){
System.out.println("插入成功");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
JdbcUtils.release(connection,pstm,null);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
输出:
防止SQL注入本质,传递字符 带有“ ”,转义字符会被转义
改进示例:
import com.cheng.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class Test {
public static void main(String[] args) {
// 正常查询
login("你好","123456");
System.out.println("=================");
// SQL注入
login("'' OR 1=1","'' OR 1=1");
}
public static void login(String username,String password) {
Connection connection = null;
PreparedStatement pstm = null;
ResultSet rs = null;
try {
connection = JdbcUtils.getConnection();
// 区别:
// 1.使用问号占位符代替参数
String sql = "SELECT * FROM users WHERE `NAME`=? AND `PASSWORD`=?";
// 2.预编译sql,先写sql然后不执行
pstm = connection.prepareStatement(sql);
// 手动赋值
pstm.setString(1, username);// 1代表第一个问号
pstm.setString(2, password);
rs = pstm.executeQuery();//注意!!!!这里不像st.executeQuery(sql);那样,括号里不要写sql,否则报错。因为PreparedStatement是预编译的,PreparedStatement对象中已包含SQL查询
//查询完毕返回结果集
while (rs.next()) {
System.out.println(rs.getString("NAME"));
}
JdbcUtils.release(connection, pstm, rs);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
JdbcUtils.release(connection, pstm, rs);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
注意!!!!rs = pstm.executeQuery();
不像rs = st.executeQuery(sql);
那样,括号里不要写sql,否则报错。因为PreparedStatement是预编译的,PreparedStatement对象中已包含SQL查询
输出:(SQL注入失效)
使用IDEA连接数据库
连接数据库
添加表
修改数据表:
双击数据表,修改,提交
编写查询
JDBC操作事务
要么都成功,要么都失败
ACID原则
- 原子性:要么全部完成,要么都不完成
- 一致性:结果总数不变
- 隔离性:多个进程互不干扰
- 持久性:一旦提交不可逆,持久化到数据库了
隔离性的问题:
- 脏读: 一个事务读取了另一个没有提交的事务
- 不可重复读:在同一个事务内,重复读取表中的数据,表发生了改变
- 虚读(幻读):在一个事务内,读取到了别人插入的数据,导致前后读出来的结果不一致
代码实现
- 开启事务conn.setAutoCommit(false);
- 一组业务执行完毕,提交事务
- 可以在catch语句中显示的定义回滚,但是默认失败会回滚
创建示例数据库:
public class JdbcTransaction{
public static void main(String[] args) {
Connection conn =null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
//关闭数据库的自动提交功能, 开启事务
conn.setAutoCommit(false);
//自动开启事务
String sql = "update account set money = money-100 where id = 1";
ps =conn.prepareStatement(sql);
ps.executeUpdate();
String sql2 = "update account set money = money-100 where id = 2";
ps=conn.prepareStatement(sql2);
ps.executeUpdate();
//业务完毕,提交事务
conn.commit();
System.out.println("操作成功");
} catch (Exception e) {
try {
//如果失败,则默认回滚
conn.rollback();//如果失败,回滚
System.out.println("失败");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
e.printStackTrace();
}finally {
try {
JdbcUtils.release(conn,ps,rs);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
数据库连接池
传统连接方式:数据库连接–执行完毕–释放
服务器频繁重复连接–释放会十分浪费资源
池化技术:准备一些预先的资源,过来就连接预先准备好的
- 常用连接数:10
- 最小连接数:10(一般跟常用连接数相等)
- 最大连接数 : 100(业务最高承载上限)
- 若大于最大连接数,排队等待,
- 等待超时:100ms(100ms后等待的链接断开连接)
编写连接池:实现一个接口DateSource
常见的开源数据源实现(拿来即用)
以下这些是市面上常见的DateSource接口的实现类
- DBCP
- C3P0
- Druid: 阿里巴巴
使用了这些数据库连接池之后,我们在项目开发中就不需要编写连接数据库的代码了
DBCP
需要用到的JAR包
(使用maven导入)
pom.xml
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.7.0</version>
</dependency>
DBCP配置文件
dbcp-config.properties
#连接设置
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&&useSSL=true
username=root
password=123456
#<!-- 初始化连接 -->
initialSize=10
#最大连接数量
maxActive=50
#<!-- 最大空闲连接 -->
maxIdle=20
#<!-- 最小空闲连接 -->
minIdle=5
#<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
maxWait=60000
#JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:【属性名=property;】
#注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
connectionProperties=useUnicode=true;characterEncoding=UTF8
#指定由连接池所创建的连接的自动提交(auto-commit)状态。
defaultAutoCommit=true
#driver default 指定由连接池所创建的连接的只读(read-only)状态。
#如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
defaultReadOnly=
#driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
#可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_UNCOMMITTED
工具类
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;
public class Dbcp {
private static DataSource dataSource = null;
static {
try{
FileInputStream in = new FileInputStream("src/main/resources/dbcpconfig.properties");
Properties properties = new Properties();
properties.load(in);
// 1.创建数据源 工厂模式——>创建
dataSource = BasicDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
// 2.获取连接
public static Connection getConnection() throws Exception{
return dataSource.getConnection(); // 从数据源中获取连接
}
// 3.释放资源
public static void release(Connection conn, Statement st, ResultSet rs) throws SQLException {
if (rs != null) {
rs.close();
}
if (st != null){
st.close();
}
if(conn != null){
conn.close();
}
}
}
测试代码
import com.cheng.utils.Dbcp;
import java.sql.*;
public class DbcpTest {
public static void main(String[] args) throws SQLException {
Connection conn =null;
PreparedStatement ps = null;
ResultSet rs =null;
try {
conn = Dbcp.getConnection();
String sql = "select * from users";
ps=conn.prepareStatement(sql);
rs=ps.executeQuery();
while (rs.next()){
System.out.println(rs.getString("NAME"));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
Dbcp.release(conn,ps,rs);
}
}
}
C3P0
需要用到的JAR包
(使用maven导入)
pom.xml
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>
C3P0配置文件
c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!-- 如果在代码中"ComboPooledDataSource ds=new ComboPooledDataSource();"这样写就表示使用的是c3p0的缺省(默认)-->
<!-- c3p0的缺省(默认)配置-->
<default-config>
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&&useSSL=true</property>
<property name="user">root</property>
<property name="password">123456</property>
<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</default-config>
<!-- 如果在代码中"ComboPooledDataSource ds=new ComboPooledDataSource("s1");"这样写就表示使用的是s1配置参数)-->
<named-config name="s1">
</named-config>
</c3p0-config>
另外,可以使用代码配置参数
// 可以使用代码配置参数
dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("");
dataSource.setJdbcUrl("");
dataSource.setUser("");
dataSource.setPassword("");
dataSource.setMaxPoolSize(100);
工具类
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class C3p0 {
private static ComboPooledDataSource dataSource = null;
static {
try{
// 创建数据源
dataSource = new ComboPooledDataSource();
} catch (Exception e) {
e.printStackTrace();
}
}
// 2.获取连接 因为getConnection()是接口的方法 所以下面跟DBCP一样都不用变
public static Connection getConnection() throws Exception{
return dataSource.getConnection(); // 从数据源中获取连接
}
// 3.释放资源
public static void release(Connection conn, Statement st, ResultSet rs) throws SQLException {
if (rs != null) {
rs.close();
}
if (st != null){
st.close();
}
if(conn != null){
conn.close();
}
}
}
测试代码
import com.cheng.utils.C3p0;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class C3p0Test {
public static void main(String[] args) throws SQLException {
Connection conn =null;
PreparedStatement ps = null;
ResultSet rs =null;
try {
conn = C3p0.getConnection();
String sql = "select * from users";
ps=conn.prepareStatement(sql);
rs=ps.executeQuery();
while (rs.next()){
System.out.println(rs.getString("NAME"));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
C3p0.release(conn,ps,rs);
}
}
}
结论
无论使用什么数据源,本质是不变的,DateSource接口不会变,方法就不会变,因为这些开源数据源都是DateSource接口的实现。