1. JDBC介绍
java语言连接数据库,JDBC通过载入不同的数据库驱动程序进而于不同的数据库进行连接。
2. 数据库驱动载入
使用文本编辑器需要配置classpath环境变量,环境变量路径配置驱动地址;使用IDEA在选择需要加载驱动的 模块->打开模块设置(Open Module Settings)->库(Libraries)-> 加载数据库驱动(+号)。
3. 数据库表的设计可以使用PowerDesign工具
4. JDBC编程六步
(1)注册数据库驱动;(2)连接数据库;(3)创建执行SQL对象;(4)执行SQL语句;(5)查询语句处理查询结果集;(6)释放资源;
5. jdbc实例,执行insert,delete,update,select语句
处理插入语句
public class JDBCTest{ public static void main(String[] args){ Connection conn = null; Statement stat = null; try{ Driver driver = new com.mysql.cj.jdbc.Driver(); //多态,父类型引用指向子类型对象 //1. 注册数据库驱动 DriverManager.registerDriver(driver); String url = "jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false"; String user = "root"; String password = "123456"; //2. 连接数据库 conn = DriverManager.getConnection(url,user,password); System.out.println(conn); //3. 获取执行SQL对象 stat = conn.createStatement(); //4. 执行SQL语句 String sql = "insert into dept_bak(deptno,dname,loc) value(60,'销售部','北京')"; int result = stat.executeUpdate(sql); System.out.println(result == 1 ? "执行成功":"执行失败"); }catch(SQLException e){ e.printStackTrace(); }finally{ //6. 释放资源 //释放资源放在finally中,不管前面是否执行成功,都释放资源 //释放资源依次从下往上释放(stat,conn),释放资源也需要处理异常 if(stat != null){ try{ stat.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } } }
注册数据库驱动使用registerDriver()方法,但是这种方法已经不常用,后面使用反射会更加常用。
处理更新,删除语句
public class jdbcTest02{ public static void main(String[] args){ Connection conn = null; Statement stat = null; try{ //1. 注册驱动 DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver()); //2. 连数据库 conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); //3. 获取执行SQL对象 stat = conn.createStatement(); //4. 执行SQL语句 //String sql = "delete from dept_bak where deptno = 50"; String sql = "update dept_bak set dname = '研发部' where deptno = 60"; int result = stat.executeUpdate(sql); System.out.println(result == 1 ? "执行成功":"执行失败"); }catch(SQLException e){ e.printStackTrace(); }finally{ //6. 释放资源 if(stat != null){ try{ stat.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } } }
其中insert,update,delete都不需要对查询结果集处理,只需根据返回结果(SQL语句执行后数据改变的条数对应返回结果数)来判断SQL语句是否执行成功。
使用反射,并且把数据库等字符串信息写入.properties文件中,使用资源绑定器读取配置文件。
public class jdbcTest04{ public static void main(String[] args){ ResourceBundle rb = ResourceBundle.getBundle("db"); String dbdriver = rb.getString("driver"); String url = rb.getString("url"); String user = rb.getString("user"); String password = rb.getString("password"); Connection conn = null; Statement stat = null; try{ Class.forName(dbdriver); conn = DriverManager.getConnection(url,user,password); stat = conn.createStatement(); String sql = "update dept_bak set dname = '研发部2',loc = '天津' where deptno = 60"; int result = stat.executeUpdate(sql); System.out.println(result == 1? "执行成功":"执行失败"); }catch(Exception e){ e.printStackTrace(); }finally{ if(stat != null){ try{ stat.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } } }
db.properties
driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false user=root password=123456
使用资源绑定器对代码部分修改较少
执行select语句,处理查询结果集
public class jdbcTest05{ public static void main(String[] args){ Connection conn = null; Statement stat = null; ResultSet rs = null; try{ Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); stat = conn.createStatement(); String sql = "select empno as a,ename,sal from emp"; rs = stat.executeQuery(sql); //执行select语句 //1. 使用getString(数据库字段名) 遍历 /* while(rs.next()){ //next()遍历查询结果集 String empno = rs.getString("empno"); String ename = rs.getString("ename"); String sal = rs.getString("sal"); System.out.println(empno + " " + ename + " " + sal); } */ //2. 使用getString(第几个字段)遍历 /* while(rs.next()){ String empno = rs.getString(1); String ename = rs.getString(2); String sal = rs.getString(3); System.out.println(empno + " " + ename + " " + sal); } */ //3. 遍历具体字段getInt(),getString(),getDouble() /* while(rs.next()){ int empno = rs.getInt("empno"); String ename = rs.getString("ename"); double sal = rs.getDouble("sal"); System.out.println(empno + " " + ename + " " + (sal + 100)); //可以进行数据运算 } */ //4. 可以遍历别名字段 while(rs.next()){ String empno = rs.getString("a"); //使用别名,遍历查询结果集 String ename = rs.getString("ename"); String sal = rs.getString("sal"); System.out.println(empno + " " + ename + " " + sal); } }catch(Exception e){ e.printStackTrace(); }finally{ if(rs != null){ try{ rs.close(); }catch(Exception e){ e.printStackTrace(); } } if(stat != null){ try{ stat.close(); }catch(Exception e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(Exception e){ e.printStackTrace(); } } } } }
与update,delete,insert语句不同的是,select语句需要处理查询结果集,处理查询结果集通常有4种方式来获取字段的值,同时在最后需要关闭查询结果集的异常。
6. SQL注入问题
SQL注入:用户输入的信息中可能含有SQL语句的关键字,并且这些关键字参与SQL语句的编译过程,导致SQL语句的查询原意扭曲,进而达到SQL注入。
使用用户登录模拟SQL注入问题
public class jdbcTest06{ public static void main(String[] args){ //初始化用户界面 Map<String,String> loginInfo = initUI(); //用户登录 boolean loginFlag = login(loginInfo); System.out.println(loginFlag? "登录成功":"登录失败"); } public static Map<String,String> initUI(){ System.out.println("请输入用户名:"); Scanner s = new Scanner(System.in); String username = s.nextLine(); System.out.println("请输入密码:"); String password = s.nextLine(); Map<String,String> map = new HashMap(); map.put("username",username); map.put("password",password); return map; } public static boolean login(Map<String,String> userloginInfo){ boolean flag = false; String getusername = userloginInfo.get("username"); String getpassword = userloginInfo.get("password"); Connection conn = null; Statement stat = null; ResultSet rs = null; try{ Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); stat = conn.createStatement(); String sql = "select * from t_user where loginname='" + getusername + "' and loginpwd = '"+getpassword+"'"; rs = stat.executeQuery(sql); if(rs.next()){ flag = true; } }catch(Exception e){ e.printStackTrace(); }finally{ if(rs != null){ try{ rs.close(); }catch(SQLException e){ e.printStackTrace(); } } if(stat != null){ try{ stat.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } return flag; } }
在t_user表有两条数据{“zhangsan”:"123","jack","123"},当使用数据库中的用户名和密码匹配时可以正常登录,使用数据库中没有的数据也可以登录失败,但是当使用用户名:fdsa,密码:fdsa' or '1'='1,也可以登录成功,这是因为用户名和密码正好完成了SQL语句的拼接(select * from t_user where loginname = 'fdsa' and loginpwd = 'fdsa' or '1'='1' or '1' = '1';),执行了非法的SQL编译。
7. 解决SQL注入问题
使用prepareStatement创建预编译数据库对象
public class jdbcTest07{ public static void main(String[] args){ Map<String,String> userloginInfo = InitUI(); boolean result = useLogin(userloginInfo); System.out.println(result? "登录成功":"登录失败"); } public static Map<String,String> InitUI(){ System.out.println("请输入用户名:"); Scanner s = new Scanner(System.in); String username = s.nextLine(); System.out.println("请输入密码:"); String password = s.nextLine(); Map<String,String> map = new HashMap<>(); map.put("username",username); map.put("password",password); return map; } public static boolean useLogin(Map<String,String> map){ boolean flag = false; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; String username = map.get("username"); String password = map.get("password"); try{ Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); String sql = "select * from t_user where loginname = ? and loginpwd = ?"; //?表示占位符,用来接收值 ps = conn.prepareStatement(sql); //创建预编译数据库操作对象 ps.setString(1,username); //对占位符1传入值 ps.setString(2,password); //对占位符2传入password,loginname和loginpwd都是字符串,所以使用setString(),如果是Int,则使用setInt() rs = ps.executeQuery(); if(rs.next()){ flag = true; } }catch(Exception e){ e.printStackTrace(); }finally{ if(rs != null){ try{ rs.close(); }catch(SQLException e){ e.printStackTrace(); } } if(ps != null){ try{ ps.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } return flag; } }
使用prepareStatement解决SQL注入问题原理:先编译SQL语句框架,然后填入变量值,就算有SQL关键字,也不会使关键字参与SQL编译。
使用prepareStatement执行insert,update,delete语句
public class jdbcTest08{ public static void main(String[] args){ Connection conn = null; PreparedStatement ps= null; try{ Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); /* String sql = "insert into dept_bak(deptno,dname,loc) values(?,?,?)"; ps = conn.prepareStatement(sql); ps.setInt(1,70); ps.setString(2,"研发二部"); ps.setString(3,"北京"); */ /* String sql = "update dept_bak set dname = ?,loc = ? where deptno = ?"; ps = conn.prepareStatement(sql); ps.setString(1,"研发三部"); ps.setString(2,"上海"); ps.setInt(3,70); */ String sql = "delete from dept_bak where deptno = ?"; ps = conn.prepareStatement(sql); ps.setInt(1,70); int result = ps.executeUpdate(); System.out.println(result); }catch(Exception e){ e.printStackTrace(); }finally{ if(ps != null){ try{ ps.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } } }
8. createStatement和prepareStatement比较
在一些场景中可能需要使用createStatement来实现某些功能,例如排序等场景
public class jdbcTest02 { public static void main(String[] args) { //1. 使用PreparedStatement不能满足排序场景 /*System.out.println("请输入asc或desc进行升序或降序:"); Scanner scanner = new Scanner(System.in); String str = scanner.nextLine(); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); String sql = "select * from emp order by ename ?"; ps = conn.prepareStatement(sql); ps.setString(1,str); rs = ps.executeQuery(); if(rs.next()){ System.out.println(rs.getString("ename")); } } catch (Exception e) { e.printStackTrace(); } finally { if(rs != null){ try{ rs.close(); }catch (SQLException e){ e.printStackTrace(); } } if(ps != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn != null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }*/ //2. 使用Statement满足升序排序场景 System.out.println("请输入asc或desc进行升序或降序:"); Scanner scanner = new Scanner(System.in); String str = scanner.nextLine(); Connection conn = null; Statement stat = null; ResultSet rs = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); stat = conn.createStatement(); String sql = "select * from emp order by ename " + str; rs = stat.executeQuery(sql); while(rs.next()){ System.out.println(rs.getString("ename")); } } catch (Exception e) { e.printStackTrace(); } finally { if(rs != null){ try{ rs.close(); }catch (SQLException e){ e.printStackTrace(); } } if(stat != null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn != null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
在排序场景中我们需要asc或desc来参与SQL语句的编译,如果使用prepareStatement那么把排序当作字符串处理'asc'或'desc',这样SQL语句是不能执行的,所以此时可以使用createStatement来满足业务需求,所以并不是说所有情况下都需使用prepareStatement,针对不同场景选择不同的数据库执行对象。
createStatement和prepareStatement对比:
(1)prepareStatement可解决SQL注入问题;
(2)createStatement是编译一次执行一次,preparedStatement编译一次,可执行多次,效率更高;
(3)preparedStatement会在编译阶段对类型的进行安全检查;
当业务层面需要SQL注入的情况(需要SQL语句拼接)使用createStatement,其他情况基本使用prepareStatement。
9. jdbc事务
jdbc事务是自动提交的,执行一次,提交一次
public class jdbcTest03 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); String sql = "update dept_bak set dname = ? where deptno = ?"; ps = conn.prepareStatement(sql); ps.setString(1,"研发5部"); ps.setInt(2,60); int result = ps.executeUpdate(); //在此处打断点,当执行完这步,数据库中数据已经更新 ps.setString(1,"研发6部"); ps.setInt(2,60); result = ps.executeUpdate(); System.out.println( result == 1 ? "执行成功":"执行失败"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); }finally { if(ps != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn != null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
如果在调用executeUpdate()处打断点,当执行完语句,数据库中值已经更新,可以看出事务是自动提交的,这显然不符合业务需求,如果是转账的操作那么账户1已经减去了一部分钱,而账户2还没有收到。
模拟转账场景
public class jdbcTest04 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); String sql = "update t_act set balance = ? where actno = ?"; ps = conn.prepareStatement(sql); conn.setAutoCommit(false); //开启事务,关闭自动提交 ps.setDouble(1,10000); ps.setInt(2,111); int result = ps.executeUpdate(); // String str = null; //模拟异常,由于jdbc是自动提交事务的,如果转账过程中发生异常,那么此时111账户余额已经为10000,但是222账户还没有增加 // str.toString(); ps.setDouble(1,10000); ps.setInt(2,222); result += ps.executeUpdate(); conn.commit(); //当两边都执行成功,转账成功后,提交事务 System.out.println(result == 2? "转账成功":"转账失败"); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { if(conn != null){ try { conn.rollback(); // 当执行异常时,回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); } finally { if(ps != null){ try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if(conn != null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
在转账场景中,如果转账过程中发生异常,那么账户1已经减去了10000块,而账户2并没有增加。
所有我们要在建立连接后开启事务,关闭自动提交(conn.setAutoCommit(false)),当我们的SQL语句都执行成功后才提交(conn.commit()),如果当中发生异常那么必须回滚(conn.rollback())。
10. jdbc中常用方法的封装
对jdbc中一些常用的方法我们可以封装到一个类中,这样我们在其他类中可以更方便的调用
public class DBUtil { //可以直接使用类名调用方法,不需要new对象,所以构造方法私有化 private DBUtil(){} static{ try{ Class.forName("com.mysql.cj.jdbc.Driver"); //可能会多次连接数据库,但是注册驱动只注册一次,静态代码块在类加载时执行,且只执行一次 }catch(ClassNotFoundException e){ e.printStackTrace(); } } //获取数据库连接对象 public static Connection getConnection() throws SQLException{ //使用抛出异常,不用捕获异常,因为在主方法中已经进行捕获 return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456"); } //关闭资源 public static void close(Connection conn,Statement stat,ResultSet rs){ if(rs != null){ try{ rs.close(); }catch(SQLException e){ e.printStackTrace(); } } if(stat != null){ try{ stat.close(); }catch(SQLException e){ e.printStackTrace(); } } if(conn != null){ try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } }
使用DBUtil工具类实现模糊查询
public class jdbcTest05 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); String sql = "select * from emp where ename like ?"; ps = conn.prepareStatement(sql); ps.setString(1,"_A%"); //模糊查询,ename中第二个字母是'A' rs = ps.executeQuery(); while(rs.next()){ System.out.println(rs.getString("ename")); } }catch (Exception e){ e.printStackTrace(); }finally { DBUtil.close(conn,ps,rs); } } }
11. 行级锁
select ename,job,sal from emp where job = 'manager' for update;
当在select语句之后添加for update之后就表示给查询处理的整行完整数据添加了行级锁,当添加了行级锁之后,事务2就不能对数据进行修改了,必须等待事务1提交之后才能进行修改数据
使用两个事务1,2模拟行级锁
/** * 演示行级锁 * 事务1 */ public class jdbcTest06 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try{ conn = DBUtil.getConnection(); conn.setAutoCommit(false); String sql = "select ename,job,sal from emp_bak where job = ? for update"; ps = conn.prepareStatement(sql); ps.setString(1,"MANAGER"); rs = ps.executeQuery(); while (rs.next()){ System.out.println(rs.getString("ename") + " "+ rs.getString("job") + " "+ rs.getDouble("sal")); } conn.commit(); //添加行级锁后,在此处打断点,模拟事务1还没执行完查询,那么事务2(jdbcTest07)就不能进行更新,没有返回值,当事务1执行完成后,事务2才执行更新 }catch (Exception e){ if(conn != null){ try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); }finally { DBUtil.close(conn,ps,rs); } } }
/** * 演示行级锁 * 事务2 */ public class jdbcTest07 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String sql = "update emp_bak set sal = sal * 1.1 where job = ?"; ps = conn.prepareStatement(sql); ps.setString(1,"MANAGER"); int result = ps.executeUpdate(); System.out.println(result); conn.commit(); } catch (Exception e) { if(conn != null){ try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); }finally { DBUtil.close(conn,ps,null); } } }