什么是JDBC编程?
全称Java Database Connectivity。简单来说就是使用Java里面提供的一些类和方法,利用程序链接数据库,进行增删改查操作。这个过程就叫做JDBC编程。
MySQL数据库操作
连接数据库步骤
前置准备
- 安装MySQL Connector J 8.0。
 导入mysql-connector-java-8.0.20.jar包(通过驱动里面的API访问MySQL数据库)。
程序中使用步骤
注册驱动(指明使用什么驱动来连接数据库)。
- 建立连接。
 - 发起请求。
 - 处理数据。
 - 关闭连接
 
基本使用:
package com.desiki.jdbc;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) {try {//1、注册驱动(使用什么驱动来连接数据库)Class.forName("com.mysql.cj.jdbc.Driver");//2、建立连接String url = "jdbc:mysql://localhost:3306/mygamedb?useUnicode=true&characterEncoding=UTF8&useSSL=true&&serverTimezone=GMT";String user = "root";String password = "root";Connection con = DriverManager.getConnection(url, user, password);//3、发起请求Statement stmt = con.createStatement();ResultSet rs = stmt.executeQuery("select * from users");//4、处理数据while(rs.next()){System.out.println(rs.getInt(1)+","+rs.getString(2)+","+rs.getString(3)+","+rs.getDate(4));}} catch (Exception e) {e.printStackTrace();} finally {//5、关闭连接try {if (rs != null)rs.close();} catch (SQLException e) {e.printStackTrace();}try {if (stmt != null)stmt.close();} catch (SQLException e) {e.printStackTrace();}try {if (con != null)con.close();} catch (SQLException e) {e.printStackTrace();}//Connection,Statement,ResultSet不能放在一起,需要把每个单独try catch}}}
注意:JDBC中The server time zone value ‘?й???????’ is unrecognized …… 的错误
java.sql.SQLException: The server time zone value '?й???????' is unrecognized or represents more than one time zone.You must configure either the server or JDBC driver (via the 'serverTimezone' configuration property) to use a more specifc time zone valueif you want to utilize time zone support.
出现这个的原因是因为 mysql返回的时间总是有问题,比实际时间要早8小时。
解决办法:
在jdbc连接的url后面加上serverTimezone=GMT即可解决问题,如果需要使用gmt+8时区,需要写成GMT%2B8。
例如:
将
String url=”jdbc:mysql://localhost:3306/student?useSSL=true”;
改为
String uri=”jdbc:mysql://localhost:3306/student?useSSL=true&serverTimezone=GMT”;
提取工具类JDBCUtils
由于每次进行数据库操作时都需要上述几部操作,而且像注册驱动,建立连接,关闭连接这些代码都是重复的,故提取出一个工具类JDBCUtils.java来优化程序。
package com.desiki.jdbc;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;public class JDBCUtils {private static final String connectionURL="jdbc:mysql://localhost:3306/mygamedb?useUnicode=true&characterEncoding=UTF8&useSSL=true&&serverTimezone=GMT";private static final String username = "root";private static final String password = "root";public static Connection getConnection() {try {Class.forName("com.mysql.cj.jdbc.Driver");return DriverManager.getConnection(connectionURL, username, password);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}public static void close(ResultSet rs,Statement stmt,Connection con) {closeResultSet(rs);closeStatement(stmt);closeConnection(con);}public static void close(Statement stmt1,Statement stmt2,Connection con) {closeStatement(stmt1);closeStatement(stmt2);closeConnection(con);}private static void closeResultSet(ResultSet rs ) {try {if(rs!=null)rs.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeStatement(Statement stmt) {try {if(stmt!=null)stmt.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeConnection(Connection con) {try {if(con!=null)con.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
JDBC操作(增删改查)
查询
查询所有数据
public static void selectAll() {Connection con = null;Statement stmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();stmt = con.createStatement();rs = stmt.executeQuery("select * from users");//excuteXXX//跟iterator遍历器类似while(rs.next()) {// 通过列的索引来取数据,索引从1开始System.out.println(rs.getInt(1)+","+rs.getString(2)+","+rs.getString(3));// 通过列的列名来取数据System.out.println(rs.getInt("id")+","+rs.getString("username")+","+rs.getString("password"));}} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, stmt, con);}}
查询带参数的数据
错误用法:使用Statement,会导致SQL注入。
举例:用户名密码验证。
public static Boolean verticy(String username, String password) {Connection con = null;Statement stmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();stmt = con.createStatement();String sql = "select * from users where username='" + username + "' and password = '" + password + "'";System.out.println(sql);rs = stmt.executeQuery(sql);if (rs.next()) {return true;} else {return false;}} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, stmt, con);}return false;}
正常情况,在main方法中测试成功:
public static void main(String[] args) {System.out.println(verticy("azure", "123"));//用户名密码都存在且正确,输出true,合理}
但是如果这么输入参数,用户名密码都不正确,同样也验证成功:
public static void main(String[] args) {System.out.println(verticy("azure123", "1213' or '1' = '1"));//用户名密码不正确,也输出了true}
这就是SQL注入。
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
上述代码中验证了两个用户名密码的登录。
第一条因为用户名密码都正确,输出true,验证成功,合情合理。
第二条用户名密码都不正确,居然也验证成功了,这是为什么呢?
原因在于我们组拼的sql语句是有漏洞的,比如上述第二条sql组拼后:select * from users where username='azure123' and password = '1213' or '1' = '1'
条件1=1 恒为true,所以用户名和密码正不正确无所谓,这个条件一定会执行成功。
正确方法:使用PreparedStatement
语法格式:
String sql = "select * from users where username = ? and password = ?";//?代表参数PreparedStatement pstmt = conn.prepareStatement(sql);//设置参数索引,从1开始pstmt.setString(1,xxx);pstmt.setString(2,xxx);ResultSet rs = pstmt.executeQuery();
正确的用户名密码验证示例:
public static boolean verticy(String username,String password) {Connection con = null;PreparedStatement pstmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();String sql = "select * from users where username = ? and password = ?";pstmt = con.prepareStatement(sql);pstmt.setString(1, username);pstmt.setString(2, password);rs = pstmt.executeQuery();if(rs.next())return true;elsereturn false;} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}finally {JDBCUtils.close(rs, pstmt, con);}return false;}
这样可以防止SQL注入。一般来说,对于带参数的数据库操作,都是使用PreparedStatement而不是Statement。
分页查询
语法:
limit ?,?
- 第一个问号:用于指定查询记录的起始行数(从哪一行开始,行数从0开始)。
 - 第二个问号:用于指定查询数据所返回的记录行数(指查询几行)。
 
公式:
如果要查询 第7页,每页8行:
0-7  第一页
8-15 第二页
16-23 第三页
…
起始行数:(页数-1)*8
查询行数:自定义,这里是8
所以最终的结果是limit 48,8。
// pageNumber是页数,第几页 pageCount是每页显示多少条数据public static void selectUserByPage(int pageNumber,int pageCount) {Connection con = null;PreparedStatement pstmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();String sql = "select * from users limit ?,?";stmt = con.prepareStatement(sql);stmt.setInt(1, (pageNumber-1)*pageCount );stmt.setInt(2, pageCount);rs = stmt.executeQuery();//跟iterator遍历器while(rs.next()) {// System.out.println(rs.getInt(1)+","+rs.getString(2)+","+rs.getString(3));System.out.println(rs.getInt("id")+","+rs.getString("username")+","+rs.getString("password"));}} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, pstmt, con);}}
插入
public static void insert( String username,String password ) {Connection con = null;PreparedStatement stmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();String sql = "insert into users(username,password) values(?,?)";stmt = con.prepareStatement(sql);stmt.setString(1, username);stmt.setString(2, password);int result =stmt.executeUpdate();// 返回值代表收到影响的行数} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, stmt, con);}}
删除
//一般根据id来删除数据public static void delete(int id) {Connection con = null;PreparedStatement stmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();String sql = "delete from user where id = ?";stmt = con.prepareStatement(sql);stmt.setInt(1, id);int result =stmt.executeUpdate();// 返回值代表收到影响的行数if(result>0) {System.out.println("删除成功");}else {System.out.println("删除失败");}} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, stmt, con);}}
修改
正常修改一条或多条记录
//根据id修改密码public static void update(int id,String newPassword) {Connection con = null;PreparedStatement stmt = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();String sql = "update usesr set password = ? where id = ?";stmt = con.prepareStatement(sql);stmt.setString(1, newPassword);stmt.setInt(2, id);int result =stmt.executeUpdate();// 返回值代表收到影响的行数if(result>0) {System.out.println("修改成功");}else {System.out.println("修改失败");}} catch (Exception e) {e.printStackTrace();} finally {JDBCUtils.close(rs, stmt, con);}}
事务操作
理解事务之前,先讲一个日常生活中最常干的事:转账
从A账户中转1000块给B账户,转账这个过程必须是一个事务。否则,可能会由于各种原因而导致,A账户减少了1000块,但B账户没有相应的增加1000块,或者 B账户增加了1000块,但A账户没有减少1000块。一个步骤成功而另一个步骤失败,这样不是你用户损失就是银行损失。所以,为了保证转账前后的一致性,就必须确保转账操作是一个事务。这样,不管哪一个步骤失败了以后,就可以回滚到转账前的状态,就像这个事务从来没有执行过一样,对双方都有利。
事务就是用来解决类似问题的。事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。
当你需要一次执行多条SQL语句时,可以使用事务。通俗一点说,就是,如果这几条SQL语句全部执行成功,则才对数据库进行一次更新,如果有一条SQL语句执行失败,则这几条SQL语句全部不进行执行。这个时候需要用到事务。参考1
JDBC是Java数据库连接相关的API,所以Java的事务管理也是在通过该API进行的。JDBC的核心是Connection接口,JDBC的事务管理是基于Connection接口来实现的,通过Connection对象进行事务管理。
JDBC对事务的处理规则,必须是基于同一个Connection对象的。
JDBC提供了3个方法来进行事务管理:
- setAutoCommit() 设置自动提交,方法中需要传入一个boolean类型的参数,true为自动提交,false为手动提交
 - commit() 提交事务
 - rollback() 回滚事务(回滚一般写在catch块中)
 
JDBC默认的事务处理行为是自动提交。所以JDBC在进行事务管理时,首先要通过Connection对象调用setAutoCommit(false) 方法, 将SQL语句的提交(commit)由驱动程序转交给应用程序负责。并且调用setAutoCommit(false) 方法后,程序必须调用commit或者rollback方法,否则SQL语句不会被提交或回滚。参考2
public static void transferAccounts(String username1,String username2,int money) {Connection con = null;PreparedStatement stmt1 = null;PreparedStatement stmt2 = null;ResultSet rs = null;try {con = JDBCUtils.getConnection();con.setAutoCommit(false);//设置手动提交事务//转出String sql = "update user set balance = balance - ? where username = ?";stmt1 = con.prepareStatement(sql);stmt1.setInt(1, money);stmt1.setString(2, username1);stmt1.executeUpdate();// 返回值代表收到影响的行数//转入sql = "update user set balance = balance + ? where username = ?";stmt2 = con.prepareStatement(sql);stmt2.setInt(1, money);stmt2.setString(2, username2);stmt2.executeUpdate();// 返回值代表收到影响的行数con.commit();//提交事务} catch (Exception e) {try {con.rollback();//只要出现异常。就回滚!} catch (SQLException e1) {e1.printStackTrace();}} finally {JDBCUtils.close(stmt2, stmt1, con);}}
连接池(数据源)
对于现在的JDBCUtils,其实还有些问题。每次数据库操作都需要建立一个新的连接,使用完后再关闭掉。假如有大量的用户使用,会造成性能内存的极大开销,对此,我们可以使用连接池(数据源)。
连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。
这篇文档连接池部分写的比较简单,具体可以看这篇JDBC连接池。
改良JDBCUtils
给现有的JDBCUtils加入连接池。
package com.desiki.jdbc01;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.ArrayList;public class JDBCUtils {private static final String connectionURL="jdbc:mysql://localhost:3306/mygamedb?useUnicode=true&characterEncoding=UTF8&useSSL=true&&serverTimezone=GMT";private static final String username = "root";private static final String password = "root";private static ArrayList<Connection> conList = new ArrayList<Connection>();static {//设置5个初始连接for(int i =0;i<5;i++) {Connection con = createConnection();conList.add(con);}}public static Connection getConnection() {if(conList.isEmpty()==false) {Connection con = conList.get(0);conList.remove(con);return con;}else {return createConnection();}}private static Connection createConnection() {try {Class.forName("com.mysql.cj.jdbc.Driver");return DriverManager.getConnection(connectionURL, username, password);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}public static void close(ResultSet rs,Statement stmt,Connection con) {closeResultSet(rs);closeStatement(stmt);closeConnection(con);}public static void close(Statement stmt1,Statement stmt2,Connection con) {closeStatement(stmt1);closeStatement(stmt2);closeConnection(con);}private static void closeResultSet(ResultSet rs ) {try {if(rs!=null)rs.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeStatement(Statement stmt) {try {if(stmt!=null)stmt.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeConnection(Connection con) {// try {// if(con!=null)con.close();// } catch (SQLException e) {// // TODO Auto-generated catch block// e.printStackTrace();// }conList.add(con);}}
使用方式和之前完全一样。
对于我们自己写的连接池,不是很完善,比较简陋,很多东西没考虑到,一般可以用第三方的数据源。这里介绍两种:DBCB,C3P0。
DBCP
DBCP(DataBase Connection Pool)是Java数据库连接池的一种,由Apache开发,通过数据库连接池,可以让程序自动管理数据库连接的释放和断开。
使用步骤:
下载本体commons-dbcp2-2.7.0-bin.jar包。下载地址: http://commons.apache.org/proper/commons-dbcp/download_dbcp.cgi
依赖包commons-pool2-2.8.0-bin.jar包。下载地址:http://commons.apache.org/proper/commons-pool/download_pool.cgi
依赖包commons-logging-1.2-bin.jar包。下载地址:http://commons.apache.org/proper/commons-logging/download_logging.cgi
package com.desiki.jdbc01;import java.sql.Connection;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import org.apache.commons.dbcp2.BasicDataSource;public class DBCPDataSource {private static final String connectionURL="jdbc:mysql://localhost:3306/mygamedb?useUnicode=true&characterEncoding=UTF8&useSSL=true&&serverTimezone=GMT" ;private static final String username = "root";private static final String password = "root";private static BasicDataSource ds;static {//初始化dbcp数据源ds = new BasicDataSource();ds.setDriverClassName("com.mysql.cj.jdbc.Driver");ds.setUrl(connectionURL);ds.setUsername(username);ds.setPassword(password);ds.setInitialSize(5);//初始化连接的个数ds.setMaxTotal(20);//连接的最大个数ds.setMinIdle(3);//保证连接池中最少有几个空闲的备用连接}public static Connection getConnection() {try {return ds.getConnection();//通过dbcp得到的链接,不需要归还,直接close就可以,} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}public static void close(ResultSet rs,Statement stmt,Connection con) {closeResultSet(rs);closeStatement(stmt);closeConnection(con);}public static void close(Statement stmt1,Statement stmt2,Connection con) {closeStatement(stmt1);closeStatement(stmt2);closeConnection(con);}private static void closeResultSet(ResultSet rs ) {try {if(rs!=null)rs.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeStatement(Statement stmt) {try {if(stmt!=null)stmt.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeConnection(Connection con) {try {if(con!=null)con.close();//这里会把链接归还给dbcp连接池,并不是真正的断开链接} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
C3P0
快速入门:https://www.mchange.com/projects/c3p0/
下载地址:https://sourceforge.net/projects/c3p0/
注意:除去导入本体jar包外还需要导入mchange-commons-java-0.2.19.jar包
package com.desiki.jdbc01;import java.beans.PropertyVetoException;import java.sql.Connection;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import com.mchange.v2.c3p0.ComboPooledDataSource;public class C3P0DataSource {private static final String connectionURL="jdbc:mysql://localhost:3306/mygamedb?useUnicode=true&characterEncoding=UTF8&useSSL=true&&serverTimezone=GMT" ;private static final String username = "root";private static final String password = "root";private static ComboPooledDataSource ds ;static {try {ds = new ComboPooledDataSource();ds.setDriverClass("com.mysql.cj.jdbc.Driver");ds.setJdbcUrl(connectionURL);ds.setUser(username);ds.setPassword(password);ds.setInitialPoolSize(5);ds.setMaxPoolSize(20);} catch (PropertyVetoException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public static Connection getConnection() {try {return ds.getConnection();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}public static void close(ResultSet rs,Statement stmt,Connection con) {closeResultSet(rs);closeStatement(stmt);closeConnection(con);}public static void close(Statement stmt1,Statement stmt2,Connection con) {closeStatement(stmt1);closeStatement(stmt2);closeConnection(con);}private static void closeResultSet(ResultSet rs ) {try {if(rs!=null)rs.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeStatement(Statement stmt) {try {if(stmt!=null)stmt.close();} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}private static void closeConnection(Connection con) {try {if(con!=null)con.close();//这里会把链接归还给c3p0连接池,并不是真正的断开链接} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
