docker 创建MySQL
一、简介
Java DataBase Connectivity ,是Java程序访问数据库的标准接口
Java访问DB的时候,并不是直接通过TCP连接的,而是通过JDBC接口,而JDBC接口又是通过JDBC驱动来访问的
JDBC是Java标准库自带的,具体的JDBC驱动是由数据库厂商提供的,所以JBDC借口都是统一的
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌───────────────┐ │
│ Java App │
│ └───────────────┘ │
│
│ ▼ │
┌───────────────┐
│ │JDBC Interface │◀─┼─── JDK
└───────────────┘
│ │ │
▼
│ ┌───────────────┐ │
│ MySQL Driver │◀───── Oracle
│ └───────────────┘ │
│
└ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┘
▼
┌───────────────┐
│ MySQL │
└───────────────┘
一个MySQL的驱动就是一个jar包,本身也是纯java编写的,我们自己的代码只需要引用java.sql.*接口,接口再通过MySQL驱动的jar包访问DB
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌───────────────┐
│ │ App.class │ │
└───────────────┘
│ │ │
▼
│ ┌───────────────┐ │
│ java.sql.* │
│ └───────────────┘ │
│
│ ▼ │
┌───────────────┐ TCP ┌───────────────┐
│ │ mysql-xxx.jar │──┼────────▶│ MySQL │
└───────────────┘ └───────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
JVM
二、JDBC查询
2.1 驱动
JDBC是一套标准的规范接口,在 java.sql 下 , 具体的实现类在厂家提供的驱动中
maven 添加驱动
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
创造一些测试数据
-- 创建数据库learjdbc:
DROP DATABASE IF EXISTS learnjdbc;
CREATE DATABASE learnjdbc;
-- 创建登录用户learn/口令learnpassword
CREATE USER IF NOT EXISTS learn@'%' IDENTIFIED BY 'learnpassword';
GRANT ALL PRIVILEGES ON learnjdbc.* TO learn@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- 创建表students:
USE learnjdbc;
CREATE TABLE students (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
gender TINYINT(1) NOT NULL,
grade INT NOT NULL,
score INT NOT NULL,
PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入初始数据:
INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88);
INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95);
INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93);
INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100);
INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96);
INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99);
INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86);
INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79);
INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85);
INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90);
INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91);
INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);
2.2 连接
使用JDBC 需要首先了解什么是Connection:一个jdbc连接,相当于java程序到数据库的连接,链接DB需要:URL username pw 口令
url:jdbc:mysql://:/?key1=value1&key2=value2
jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8
useSSL=false&characterEncoding=utf8 不使用SSL加密,使用UTF-8作为字符编码
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();
核心代码是DriverManager 提供的静态方法getConnection()。Driver会自动扫描classpath,找到所有的JDBC驱动,然后根据URL挑选一个合适的驱动
因为JDBC连接是昂贵的资源,用后及时释放 用 try(resource)来自动释放JDBC是一个好方法
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
...
}
2.3 查询
package com.ifeng;
import java.sql.*;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args ) throws SQLException {
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/learnjdbc";
String JDBC_USER = "root";
String JDBC_PASSWORD = "123456";
Connection connection;
Statement statement = null;
ResultSet resultSet = null;
//获取链接
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
statement = connection.createStatement();
resultSet = statement.executeQuery("select * from students");
while (resultSet.next()){
Long id = resultSet.getLong(1);
String name = resultSet.getString(2);
String gender = resultSet.getString(3);
String grade = resultSet.getString(4);
String score = resultSet.getString(5);
System.out.println("id = " + id + " , name = " + name + " , gender = " + gender + " , grade = " + grade + " , score = " + score);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if(statement != null) statement.close();
if(resultSet != null) resultSet.close();
}
}
}
statement & resultSet 都是稀缺资源,及时关闭
resultSet.next() 用于判断是否有下一行,如果有 自动移入下一行
ResultSet 获取列时,索引从1开始
2.4 SQL注入
使用statement 非常容易引发SQL注入的问题
例如:验证登陆的方法:
User login(String name, String pass) {
...
stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
...
}
name & pass 都是从前端传过来的
name = "bob' OR pass=", pass = " OR pass='"
SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
避免SQL注入,针对所有的字符串进行转义,但很麻烦
还有一个方法就是PreparedStatement :Prepared 始终使用 ? 作为占位符,并且把数据 & SQL 本身传递给DB
User login(String name, String pass) {
...
String sql = "SELECT * FROM user WHERE login=? AND pass=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, name);
ps.setObject(2, pass);
...
}
改造上面的查询
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement("select * from students WHERE gender=? AND grade=?");
preparedStatement.setObject(1,"M"); // 设置占位符的值
preparedStatement.setObject(2,3);
resultSet1 = preparedStatement.executeQuery();// 最后仍然是ResultSet
while (resultSet1.next()){
Long id = resultSet1.getLong(1);
String name = resultSet1.getString(2);
String gender = resultSet1.getString(3);
String grade = resultSet1.getString(4);
String score = resultSet1.getString(5);
System.out.println("id = " + id + " , name = " + name + " , gender = " + gender + " , grade = " + grade + " , score = " + score);
}
}
三、JDBC更新
DB操作总结起来就是增删改查,CRUD:Create Retrieve Update Delete
查询用上面的PreparedStatement
3.1 插入
insert ,本质上也是用PreparedStatement执行一条SQL,不过执行者不是executeQuery() 而是executeUpdate()
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO students (id, grade, name, gender, score) VALUES (?,?,?,?,?)");
preparedStatement.setObject(1,10);
preparedStatement.setObject(2,3);
preparedStatement.setObject(3,"Bob");
preparedStatement.setObject(4,2);
preparedStatement.setObject(5,101);
int i = preparedStatement.executeUpdate(); // 使用executeUpdate来更新
System.out.println("executeUpdate = " + i);
设置参数和查询是一样的,有几个?占位符就设置几个对应参数,
要严格执行不能手动拼接SQL字符串的原则,避免安全漏洞
3.2 插入并获取主键
表设置了自增主键,insert后 数据库会自动分配主键,如何获取主键?
在创建PreparedStatement的时候,指定一个RETURN_GENERATED_KEYS标志位,表示JDBC驱动必须返回插入的自增主键
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(
"INSERT INTO students (id, grade, name, gender, score) VALUES (?,?,?,?)",
Statement.RETURN_GENERATED_KEYS
);
preparedStatement.setObject(1,5);
preparedStatement.setObject(2,"Jerry");
preparedStatement.setObject(3,2);
preparedStatement.setObject(4,101);
int i = preparedStatement.executeUpdate(); // 使用executeUpdate来更新
try(ResultSet rs = preparedStatement.getGeneratedKeys()){
if (rs.next()) {
System.out.println(rs.getLong(1));
}
}
3.3 更新
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
ps.setObject(1, "Bob"); // 注意:索引从1开始
ps.setObject(2, 999);
int n = ps.executeUpdate(); // 返回更新的行数
}
}
3.4 删除
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
ps.setObject(1, 999); // 注意:索引从1开始
int n = ps.executeUpdate(); // 删除的行数
}
}
四、事务
DB事务(Transaction)由若干个SQL语句构成一个操作序列,类似Java的synchronized同步,一个SQL要么全部成功,或者全部不成功:ACID
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
数据库事务可以并发执行,从效率角度,定义了不同的隔离级别
- 脏读:A事务读到B事务更新但未提交的数据,B回滚了
- 不重复读:A事务第一次读取数据后,B事务修改了数据,A又读取数据,两次数据不一致
- 幻读:A事务查询记录,没有,然后更新这条记录,竟然成功了
在JDBC中执行事物,本质是把多条SQL包裹在一个数据库事务中执行
Connect conn = openConnection();
try{
//关闭自动提交
conn.setAutoCommit(false);
//执行多条SQL
insert();update();delete;
//提交事物
conn.commit();
}catch(SQLException e){
// 回滚事物;
conn.rollback();
}
conn.setAtuoCommit(false) 代表自动提交关闭
conn.commit() 手动提交
//可以设定隔离级别,默认REPEATABLE_READ
conn.setTrasactionIsolation(Connection.TRANSACTION_READ_COMMITTED)
五、Batch
批量操作,可通过循环来执行PreparedStatement来执行,但是性能很低。只有参数不同若干语句可以作为batch执行(批量),它有特别的优化,速度远远快于循环执行的每个SQL
public class JDBCTest5
{
public static void main( String[] args ) throws SQLException {
List<Student> students = List.of(
new Student("小红", 20L,3L,48)
,new Student("小红2", 20L,3L,49)
,new Student("小红3", 20L,3L,50)
,new Student("小红4", 20L,3L,49)
,new Student("小红5", 20L,3L,55)
);
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/learnjdbc";
String JDBC_USER = "root";
String JDBC_PASSWORD = "123456";
Connection connection;
Statement statement = null;
ResultSet resultSet1 = null;
//获取链接
try {
connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement preparedStatement = connection.prepareStatement(
"INSERT INTO students (grade, name, gender, score) VALUES (?,?,?,?)"
);
//对于同一个PreparedStatement反复设置参数并调用addBatch()
for(Student s : students){
preparedStatement.setObject(1,s.grade);
preparedStatement.setObject(2,s.name);
preparedStatement.setObject(3,s.gender);
preparedStatement.setObject(4,s.score);
preparedStatement.addBatch();//添加到batch
}
//执行batch
int[] executeBatch = preparedStatement.executeBatch();
for(int n:executeBatch){
System.out.println(n + " inserted.");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if(statement != null) statement.close();
if(resultSet1 != null) resultSet1.close();
}
}
}
六、连接池
JDBC连接是一种俺贵的资源,创建线程也是一种昂贵的操作,频繁的创建销毁JDBC,会造成大量消耗
JDBC链接池有一个标准的接口 javax.sql.DataSource , 接口在标准库中。要使用JDBC连接池,必须用它的实现类:
- HikariCP
- C3P0
- BoneCP
- Druid
使用最广泛的是HikariCP以此为例,先添加HikariCP的依赖
<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.9</version>
</dependency>
配置连接池
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/learnjdbc");
hikariConfig.setUsername("root");
hikariConfig.setPassword("123456");
hikariConfig.addDataSourceProperty("connectionTimeout","1000");//连接超时 1s
hikariConfig.addDataSourceProperty("idleTimeout","6000");//空闲超时 6s
hikariConfig.addDataSourceProperty("maximumPoolSize","10");//最大连接数
HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
使用连接池
connection = hikariDataSource.getConnection();
...
connection.close();
get close 都不是真的创建销毁,而是从连接池中获取 放回