相关源码会在每一个部分的末尾给出
相关表的结构:
在介绍具体的工具类之前,先来简单介绍一下JDBC的连接步骤:
1. 注册驱动
在注册驱动以前,你需要先导入mysql-connector-java-8.0.11.jar(使用的是Mysql)的包,来使用与数据库连接的方法。
DriverManager.registerDriver(new com.mysql.jdbc.Driver());//这种创建方式并不好,后续会介绍其原理,以及如何优化。
2. 建立连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/petshop", "root", "123456");
3. 创建statement
Statement st = conn.createStatement();
4. 执行sql ,得到ResultSet
String sql = "select * from girlfriends";
ResultSet rs = st.executeQuery(sql);5. 遍历结果集
while(rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println("id="+id + "=name="+name+"age="+age);
}6. 释放资源
conn,st,rs由顺序1,2,3创建,就从3,2,1开始顺序close
if (rs != null) {
try {
rs.close();
} catch (SQLException sqlEx) { } // ignore
rs = null;
}//st,conn关闭方式雷同,不赘述
以上就是一般连接的基本步骤,下面来进行工具类的实现,通过分析,实现简化与优化。
一. 连接工具类实现
首先你从基本步骤2中可以发现,将用户的sql账号与密码暴露在外,是一件很不明智的行为,不仅仅是安全问题,而且在你需要切换登陆或者切换使用别的database时是一件非常不方便的事。
于是我们把这些信息都抽离出来,放到项目的/src/jdbc.properties文件中保存
driver的具体内容是由下面这导入的jar包中的配置文件决定的,每种不同的数据库有不同的driver需要更改。
路径的/后面跟的是database的名字。
urlextra是因为在实际使用过程中,可能需要使用上述语句去避免连接时报错的问题。
name和password填写你自己的实际数据。
既然放到了属性文件中,那么我们每次进行不同的数据库连接只需要更改配置文件的内容即可,并且自动从里面读取数据,可以根据不同的数据库来实现不同的JDBC配置,比如服务器远程访问不同数据库时就很有用,需要更改tomcat中xml的配置内容。
Properties properties = new Properties();
InputStream inStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");//bin文件夹下,加载字节码时,用类加载器自动添加
properties.load(inStream);
driver = properties.getProperty("driver");
urlextra = properties.getProperty("urlextra");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");即可获得所有的属性参数。
再来说注册驱动的方式,上文中使用了
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
这种方式,这种方式和普通的注册驱动方式
Class.forName(driver);
其实是一样的,但是其实都是不必要的操作,因为mysql连接的jar在4.0版本后,jar包META-INF文件夹下services的Driver中会自动加载,所以其实会默认帮助我们完成驱动的注册。因此实际中省略这个步骤,减少new一个对象的开销。
只需要一句:
conn = DriverManager.getConnection(url+urlextra, username, password);
因此,关于连接的代码如下(properties文件中的内容上面已给出)
public class JDBCUtil { static String driver = null; static String urlextra = null; static String url = null; static String username = null; static String password = null; static { try { Properties properties = new Properties(); InputStream inStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");//bin文件夹下,加载字节码时,用类加载器自动添加 properties.load(inStream); driver = properties.getProperty("driver"); urlextra = properties.getProperty("urlextra"); url = properties.getProperty("url"); username = properties.getProperty("username"); password = properties.getProperty("password"); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConn() { Connection conn = null; try { //Class.forName(driver);//4.0版本后,可以不写,jar包META-INF文件夹下services的Driver中会自动加载 conn = DriverManager.getConnection(url+urlextra, username, password); } catch (Exception e) { e.printStackTrace(); } return conn; } }
二. 释放连接工具类实现
关于释放操作,上文已经提到过,由创建顺序1,2,3去从3,2,1开始释放,唯一需要注意的是,需要重载一下release方法,在第四段内容中会提到为何需要写2种release函数,接收3个参数和2个参数的。
代码如下:
public class JDBCUtil { public static void release(Connection conn, Statement stmt, ResultSet rs) { closeRs(rs); closeStmt(stmt); closeConn(conn); } public static void release(Connection conn, Statement stmt) { closeStmt(stmt); closeConn(conn); } private static void closeRs(ResultSet rs) { try { if (rs != null) { rs.close(); } } catch (SQLException e) { e.printStackTrace(); } finally { rs = null; } } private static void closeStmt(Statement stmt) { try { if (stmt != null) { stmt.close(); } } catch (SQLException e) { e.printStackTrace(); } finally { stmt = null; } } private static void closeConn(Connection conn) { try { if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } finally { conn = null; } } }
三. 增删查改的细节区别
目前我们都是通过使用Statement来执行sql语句,增删查改中,增删改操作所返回的是一个int,即成功执行的次数。而查的方法可以返回一个resultSet集合,所以给出一个增加和一个查询的示例。
SQL增加:
conn = JDBCUtil.getConn(); st = conn.createStatement(); String sql = "insert into girlfriends values(null , 'Misaki' , 20)"; int result = st.executeUpdate(sql); if (result > 0) { System.out.println("添加成功"); } else { System.out.println("添加失败"); }
SQL查询:
conn = JDBCUtil.getConn(); st = conn.createStatement(); String sql = "select * from girlfriends"; rs = st.executeQuery(sql); while (rs.next()) { String name = rs.getString("name"); int age = rs.getInt("age"); System.out.println(name + " " + age); }
最后,第2小节提到的重载释放函数的答案相信有的人也早已知道了,因为只有在使用查询操作时可能会需要resultSet返回集,所以需要close掉3个连接,而其他操作只需要close掉2个连接即可。
四. Statement与PreparedStatement的区别
上面所使用的情况都是直接使用Statement语句来执行excuteQuery(传入一条sql语句)
下面一个查询该女生是否存在的函数:
@Override public void exist2(int id, String girlname) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = JDBCUtil.getConn(); stmt = conn.createStatement(); String sql = "select * from GirlFriends where id=" + id + " and name='" + girlname + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { System.out.println("女生存在!"); } else { System.out.println("女生不存在!"); } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtil.release(conn, stmt, rs); } }
表面上并不存在问题,然后我们使用2条语句来测试这个函数:(测试时运用的是Dao模式,相关的操作方式可以自行搜索了解)
@org.junit.jupiter.api.Test public void test_exist2() { GirlDao dao=new GirlDaoImpl(); dao.exist2(3,"Alice' or '1=1"); System.out.println("**************************"); dao.exist2(3,"Alice"); }
运行结果如下:
你发现,第二条正常输入的语句,结果是不存在
而第一条输入的语句拼接了一个(or ‘1=1)之后,居然存在了。
这个问题就是Statement的安全性问题。
Statement的执行 ,其实是拼接sql语句的。
它先拼接sql语句,然后再一起执行。 前面拼接sql语句时, 如果变量里面带有了 数据库的关键字,那么一并认为是关键字。 不认为是普通的字符串。 认为是一整个语句。由于后面的or被识别为关键字了就一并正确执行了。
所以我们不推荐实现Statement,而建议所有代码更改为PreparedStatement来执行,下面为更改的检测存在代码:
@Override public void exist3(int id, String girlname) { Connection conn = null; Statement stmt = null; PreparedStatement ppst = null; ResultSet rs = null; try { conn = JDBCUtil.getConn(); String sql = "select * from GirlFriends where id=? and name=?"; ppst = conn.prepareStatement(sql); ppst.setInt(1, id); ppst.setString(2, girlname); rs = ppst.executeQuery(); if (rs.next()) { System.out.println("女友存在!"); } else { System.out.println("女友不存在!"); } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtil.release(conn, stmt, rs); } }
运行结果如下:
即可正确执行。
在这里补充一下两种statement语句的区别
普通的Statement语句是通过stmt本身去excute一个sql语句
而PreparedStatement语句是通过使用问号(占位符)来设置需要更改的参数(在exist3中能看到相应的代码段),提供给用户更改问号的方式,即ppst.set方法(下标从1开始,根据实际情况区分类型),然后由ppst的excute方法去执行(这时的执行方法语句中是不允许有参数的,因为已经提前设定好的需要修改的参数)
ppst会把接收到的数据统一变成字符串处理,不会看作是关键字,从而解决了问题。
其他语句的ppst版本执行语句就由读者自己去书写,两者的转化很简单,并且多去尝试可以使用的函数,你的编程思路才能更加开拓。