上一篇博客讲了java程序连接数据库的基本操作,这次进一步提高java访问数据库的安全性和便捷性:
文章目录
使用预编译语句对象防止SQL注入
用statment
又耗资源又不安全,操作数据库应该用prepareStatement
对象。因为以下原因
-
数据库接受到sql语句后还会进行编译,才能执行,就像JVM要编译执行一样,每次收到statement语句,数据库都会编译一次,这就很耗资源了。
-
prepareStatement
会先把预编译语句发给数据库,然后再传入参数,这样数据库只需要编译一次。 -
prepareStatement
对象引用着预编译后的结果,每次set都是把参数直接传给数据库,就像直接把参数传给class 文件一样。private static void testInsert() { String sql = "INSERT INTO account (name,balance,create_date,cost) values(?,?,?,?)"; try { Connection connection = JDBCUtil.getConnection(); PreparedStatement pstm = connection.prepareStatement(sql); try { pstm.setObject(1, "黎明"); pstm.setObject(2, 300); pstm.setObject(3, "2018-06-20"); pstm.setObject(4,30.0); pstm.executeUpdate(); testSelect(); }finally { JDBCUtil.close(connection,pstm); } } catch (SQLException e) { System.out.println("无法创建连接"); } }
-
preparedStatement.setObject()
的效果就是自动会帮我们选择正确的类型,不用我们自己去处理 -
preparedStatement.setXXX()
方法会自动给字符串加上单引号,不用我们加。但也因此不能把表和数值变成“?”以后再指定值,因为表和数值不能取用引号括住的形式。 -
preparedStatement.clearParameters()
可以清空设置的参数,参数除非被覆盖,否则会一直在preparedStatement中存在,这样preparedStatement被多次调用时就会一直用同一个值。
用数据池优化效率
- 操作数据库的核心是:做完一个操作就得断开连接,不要霸占数据库资源。执行完一个select就得断开,update完就得断开这样。这样就涉及大量断开和重连操作,及其耗时,所以应该用线程池来管理连接。
- 数据池会先创建一批连接,有人需要了就把连接给他用,用完了就放回到数据池。不会断开连接,这样就省下了重开连接和断开的资源。跟线程池的原理一样的。
- 我们知道,创建连接需要URL,用户名和密码,所以线程池一般都是用同一个用户或少数几个用户来操作数据库的。
C3P0数据池
C3P0 数据池需要依赖两个jar包:
- c3p0-0.9.5.2.jar
- mchange-commons-java-0.2.11.jar
只导入一个会报错的。
一般用new CombopooledDataSource()
这个构造方法来新建C3P0数据池。
-
new CombopooledDataSource()
这个构造器会通过class.getResourceAsStream()
的方式从模块下c3p0-config.xml
文件中找配置数据。位置和名字都是不能变的!By default, c3p0 will look for an XML configuration file in its classloader’s resource path under the name “/c3p0-config.xml”.
-
new CombopooledDataSource()
构造函数的参数并不是文件的路径或者文件名,而是XML文件中的named-config
标签的name
属性。 -
这个构造方法返回一个
DataSource
对象,我们可以DataSource.getConnection()
获得连接,之后就按部就班了。 -
比较关键的点是要注意
c3p0-config.xml
这个文件要放在src下,这样编译才会把它放到输出文件夹。 -
还有一个关键点就是
c3p0-config.xml
文件要包括以下内容:<c3p0-config> <!-- 使用默认的配置读取连接池对象 --> <default-config> <!-- 数据库连接参数 --> <property name="jdbcUrl">jdbc:mysql://localhost:3306/db_temp</property> <property name="user">root</property> <property name="password">admin</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">10</property> <property name="checkoutTimeout">3000</property> </default-config> <named-config name="otherc3p0"> <!-- 连接参数 --> <property name="jdbcUrl">jdbc:mysql://localhost:3306/day19</property> <property name="user">root</property> <property name="password">root</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">6666</property> <property name="checkoutTimeout">1000</property> </named-config> <named-config name="abc"> <!-- 连接参数 --> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/day18</property> <property name="user">root</property> <property name="password">root</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">6</property> <property name="checkoutTimeout">1000</property> </named-config> </c3p0-config>
-
c3p0数据池完整案例:
public class JDBC09 { public static void main(String[] args) throws SQLException { ComboPooledDataSource ds = new ComboPooledDataSource(); Connection connection = ds.getConnection(); PreparedStatement pstmt = connection.prepareStatement("select * from account"); ResultSet resultSet = pstmt.executeQuery(); JDBC_7.showRS(resultSet); } }
Durid数据池
德鲁伊数据池的性能比C3P0好,是阿里巴巴开源项目。
-
druid
跟C3P0差不多,但是配置不用XML而是用Properties文件,Properties文件就可以自己喜欢放哪里就放哪里,但最好还是叫derud.properties
然后还是放在src文件下吧 -
druid
的properties文件属性名不用死记,打开com.alibaba.druid.pool.DruidDataSourceFactory
这个类就啥都有了,比较主要的是以下几个属性,跟C3P0大同小异:url
jdbc:mysql://localhost:3306/db_nameusername
一般都是同一个用户组的人共用一个用户操作数据库password
maxActive
最多能有几个链接initialSize
一开始新建多少个链接maxWai
t 毫秒值,最多等多少毫秒就报错- 注意啊,
properties
文件不能有空格的!
-
druid
是用工厂方法新建对象的:public class JDBC_9_Durid { public static void main(String[] args) throws Exception { Properties pp = new Properties(); pp.load(JDBC_9_Durid.class.getResourceAsStream("/druid.properties")); DataSource dataSource = DruidDataSourceFactory.createDataSource(pp); Connection connection = dataSource.getConnection(); PreparedStatement pstm = connection.prepareStatement("select * from account"); ResultSet resultSet = pstm.executeQuery(); JDBC_7.showRS(resultSet); } }
用Spring的JdbcTemplate进一步优化代码
-
JdbcTemplate
是Spring框架
已经帮我们写好的一个模板,可以自动帮我们处理connection和statement,自动帮我们关闭连接这样。 -
因为是第三方,也是要导入jar包,JdbcTemplate需要的包有点多:
- spring-beans-4.1.2.RELEASE.jar
- spring-core-4.1.2.RELEASE.jar
- spring-jdbc-4.1.2.RELEASE.jar
- spring-tx-4.1.2.RELEASE.jar
- com.springsource.org.apache.commons.logging-1.1.1.jar
五个包缺一不可。
-
通过构造函数
new JdbcTemplate(dataSource)
可以传入dataSource
对象获得一个jdbcTemplate
对象, -
因为
jdbcTemplate
是线程安全的,多个线程使用它也不会发生同步问题,所以可以放在一个public static域中,大家一起用。 -
jdbcTemplate的优势就是,可以让我们把精力放在sql语句本身,而不用关心关闭连接之类的细节
JdbcTemplate的两类主要方法
创建了JdbcTemplate以后,我们就主要关心两类方法:
Update方法用于执行Select以外的sql语句
Update()
方法需要注意以下几个点
-
JdbcTemplate.Update(String sql,Object... args)
这个方法接受一个sql语句,以及一个可变参数作为参数,会调用PreparedStatement来执行命令,如果只是update(String sql)
则只调用statement。private static void testUpdate() { String sql = "Update account set balance =? where id=?"; int update = template.update(sql, 500, 18); System.out.println("update = " + update); }
-
JdbcTemplate.batchUpdate(String sql,List<Object[]> args)
可以一次执行多条语句
如果用NamedParameterJdbcTemplate
代替JdbcTemplate
就能给每个不确定的参数起别名,而不是靠顺序地插入。
-
namedParameterJdbcTemplate.update(String sql,Map<String,Object> map)
可以把别名和值组合成一个map,然后执行:private static void testInsertMap() { String sql = "update account set balance = :newBalance where name = :name "; Map<String, Object> map = new HashMap<>(); map.put("newBalance", 2000); map.put("name", "黎明"); namedParameterJdbcTemplate.update(sql, map); }
-
namedParameterJdbcTemplate.update(String sql,SqlParameterSource sps)
接受一个SqlParameterSource对象,通过4个方法确定一个参数的值和类型:-
有一个
BeanPropertiesParameterSource
类是已经写好的SqlParameterSource实现类,他可以接受一个javaBean对象,通过反射快速获得这些值,但要求是传入的
javaBean必须有别名的
get()`方法String sql = "update account set balance = :balance where name = :name "; Account account = new Account(); account.setBalance(300); account.setName("黎明"); // 要求Account对象一定要有getBalance()和getName()方法 BeanPropertySqlParameterSource bpsps = new BeanPropertySqlParameterSource(account); namedParameterJdbcTemplate.update(sql, bpsps);
-
Query各种变体用于执行select的sql语句
如果是用JdbcTemplate
,则只有4中query方法可以使用:
T jdbcTemplate.queryForObject(String sql,Class<T> class, Object... args)
- 通过顺序来确定sql中不确定的值的具体参数
- 通过传入类型信息把获得的值转换成需要的类型,其实可以看成是强转。
- 接要求返回的结果是单行单列的。
- 方法有一个重载是接受
RowMapper对象
的,那个不是强转,而是通过RowMapper
把每一行变成一个对象,然后返回。那个可以是单行多列。 - 需要
RowMapper
的形参,可以直接传入BeanPropertiesRowMapper
对象。
Map<String ,Object> jdbcTemplate.queryForMap(String sql,Object... args)
- 就把得到的表以
resultSet.getMetaDate().getColumnlabel()
的值为键,以resultSet.getObject()
为值组成Map getColumnLabel()
这个方法是,如果有给列取别名就是返回别名,否则就返回列名。- 要求返回的表时单列的。
- 就把得到的表以
List<Map<String,Object>> jdbcTemplate.queryForList(String sql,Object... args)
- 就等于把多行数据变成Map以后,把Map存在List中返回。
- 对返回的表无限制。
List<T> query(String sql, RowMapper<T> rowMapper , Object... args)
得到的每行数据,都会经过rowMapper处理,得到一个由rowMapper指定的泛型对象,最后把泛型对象放到List集合中返回。- 可以直接传入
BeanPropertiesRowMapper
对象, 他接受一个Class<T>
参数,用来生成返回对象。 BeanPropertiesRowMapper
对象是怎样指定列明对应的属性的呢?他是用getLabel()
得到的值,去匹配参数的class对象每一个有写set的属性,相同就认为是一个属性。就会setProperties。
- 可以直接传入
如果用NamedParameterJdbcTemplate
来query,跟JdbcTemplate
的query类方法没什么区别,除了以下几点
jdbcTempalte
的query类方法接受的sql是多个问号组成的参数坑,而namedParameterJdbcTemplate
接受的sql是":name" 这种冒号+字符串 形式组成的参数坑。jdbcTemplate
确定参数的方法是通过参数的顺序,排第一的参数对应第一个问号,如此类推。namedParameterJdbcTemplate
确定参数的方法有两种方法- 用
Map
对象,冒号后面的别名作为键,想要传入的参数作为值。 - 用
SqlParameterSource
对象,别名和值对应关系的逻辑上面已经说过了。
- 用
- 所以我们看到
namedParameterJdbcTemplate
的query方法跟JdbcTemplate的大同小异,只是把Object...
参数变成了map
或者sqlparameterSource
所以 ,那肯定是namedParameterJdbcTemplate
比较安全和方便。
private static void testNamedQuery() {
String sql = "select name ,balance,create_date from account where name =:name";
Account account = new Account();
account.setName("黎明");
List<Account> accountList = namedParameterJdbcTemplate.query(sql,
new BeanPropertySqlParameterSource(account),
new BeanPropertyRowMapper<>(Account.class));
for (Account accoutnInList : accountList) {
System.out.println(accoutnInList);
}
}
- 但是
namedParameterJdbcTemplate
在处理数据库中IN()等语句时,就力不从心了,两者各有所长,还是看实际运用。
最后,要不就用JdbcTemplate,要不就用NamedParameterJdbcTemplate,两者选其一,不要用同一个数据池作为参数生成两个不同类型的template。数据池是线程不安全的。