jdbc核心之PrepareStatement

来源:互联网 发布:傲剑九阴真经数据 编辑:程序博客网 时间:2024/06/06 07:38

介绍PrepareStatement之前,先用前面一直用的Statement来模拟一个登录的例子。
数据库中有表userinfo,字段为

  • id:用户id,主键
  • username:用户名
  • password:密码
  • account:账户余额
  • email:邮箱
    有条用户记录 1 jack 123456 5000 jack@qq.com
    需求:输入用户名和密码进行登录
public class JDBCDemo1 {    public static void main(String[] args) {        Connection connection = null;        Statement statement = null;        ResultSet resultSet = null;        try {            //连接数据库            connection = DBUtil.getConnection();            Scanner scanner = new Scanner(System.in);            System.out.println("输入用户名:");            String username = scanner.nextLine();            System.out.println("输入密码");            String password = scanner.nextLine();            //创建statement对象            statement = connection.createStatement();            //创建sql            String sql = "SELECT * FROM USERINFO "                    + "WHERE username='"+username+"' AND password='"+password+"'";            //执行查询            resultSet = statement.executeQuery(sql);            if(resultSet.next()){                //取余额                int account = resultSet.getInt("account");                System.out.println("登录成功,账户余额为:"+account);            }else {                System.out.println("账号或密码错误!");            }        } catch (Exception e) {            e.printStackTrace();        }finally {            DBUtil.closeConnection(connection, statement, resultSet);        }    }}

注:DBUtil是自己写第一个工具类,能获取连接和关闭连接。代码如下:

public class DBUtil {    private static String driver;    private static String url;    private static String username;    private static String password;    private static String maxactive;    private static String maxwait;    //数据库连接池    private static BasicDataSource dateSource;    /**     * ThreadLocal内部是一个Map     * key是某个线程,而value是要保存的值。     * 这样可以将某个值绑定到一个线程上。随时通过     * 该线程获取这个值。     */    private static ThreadLocal<Connection> tl;    static{        try {            tl = new ThreadLocal<>();            //加载配置文件            Properties properties = new Properties();            properties.load(new FileInputStream(new File("D:\\测试\\jdbc\\src\\config.properties")));            //读取配置文件,初始化成员变量            driver = properties.getProperty("driver");            url = properties.getProperty("url");            username = properties.getProperty("username");            password = properties.getProperty("password");            maxactive = properties.getProperty("maxactive");            maxwait = properties.getProperty("maxwait");            //测试初始化是否成功            System.out.println(driver);            System.out.println(url);            System.out.println(username);            System.out.println(password);            System.out.println(maxactive);            System.out.println(maxwait);            dateSource = new BasicDataSource();            dateSource.setDriverClassName(driver);            dateSource.setUrl(url);            dateSource.setUsername(username);            dateSource.setPassword(password);            dateSource.setMaxActive(new Integer(maxactive));            dateSource.setMaxWait(new Integer(maxwait));        } catch (Exception e) {            //静态方法块里面是不能抛出异常的            e.printStackTrace();        }    }    /*     * 从连接池获取数据库连接     */    public static Connection getConnection() throws Exception{        /**         * 哪个线程调用ThreadLocal的get方法,就         * 用这个线程作为key去threadlocal内部的         * Map中取出对应的value         */        Connection connection = dateSource.getConnection();        tl.set(connection);        return connection;    }    public static void closeConnection(){        Connection conn=tl.get();        if(conn!=null){            try {                //这个时候用的是连接池的close,其他方法是oracle的,                //连接池只是起了代理的做用                conn.close();                tl.remove();            } catch (SQLException e) {                e.printStackTrace();            }        }    }    /*     * 关闭连接     */    public static void closeConnection(Connection connection,Statement statement,ResultSet resultSet){        if(resultSet != null){            try {                resultSet.close();            } catch (SQLException e) {                e.printStackTrace();            }        }        if(statement != null){            try {                statement.close();            } catch (SQLException e) {                e.printStackTrace();            }        }        if (connection != null) {            try {                connection.close();            } catch (SQLException e) {                e.printStackTrace();            }        }    }}

属性配置文件如下

driver = oracle.jdbc.driver.OracleDriverurl = jdbc:oracle:thin:@localhost:1521:orclusername = SCOTTpassword = 123456maxactive = 5maxwait = 5000

当用户输入jack 123456的时候会登录成功显示余额(进行相关操作),但是其实你乱输入一个用户名,密码输入1’ OR ‘1’=’1结果还是会出来(会出来所有结果),因为OR的优先级低于and。1=1恒成立,这就出现安全问题,所以有些字符串不允许输入。这个就称为SQL注入攻击。

解决办法:使用PrepareStatement

两者比较:

  • Statement通常用来执行静态SQL(SQL内容不含任何动态信息)语句。
  • Statement执行非静态SQL有两个问题:
    1:若SQL语句是动态拼接的,那么会造成SQL注入攻击,如上面的情况。
    2:若仅是数值变化,而SQL语义并没有变化时,批量执行多条SQL语句对数据库性能开销大。

  • PreparedStatement专门用来执行含有动态内容的SQL语句,解决了Statement执行动态SQL的两个不足。
    1:比如解决SQL注入问题:它会严格地将?表示的参数作为一个整体的参数传给DBMS,而不会发生SQL嵌套,也就是说?表示的仅仅是参数。对于上面也就是说1’ OR ‘1’=’1会当成一整个串(密码)来处理。
    2:执行的是预编译SQL,将动态信息以”?”的形式占位。数据库在接收预编译SQL后,会生成执行计划,我们接下来的工作就是传入动态信息即可,多次执行也无非就是多次传入动态信息。但可以重用同一个计划了(这就好像java程序中定义方法,动态信息被定义为参数,多次调用方法传入不同参数即可,但是方法不用每次都重新定义)。从而解决了第二个问题。

PrepareStatement
用PrepareStatement重现上面的例子

public class JDBCDemo2 {    public static void main(String[] args) {        Connection connection = null;        Statement statement = null;        ResultSet resultSet = null;        try {            //连接数据库            connection = DBUtil.getConnection();            Scanner scanner = new Scanner(System.in);            System.out.println("输入用户名:");            String username = scanner.nextLine();            System.out.println("输入密码");            String password = scanner.nextLine();            //1、创建sql            String sql = "SELECT * FROM USERINFO "                    + "WHERE username= ? AND password= ?";            /**             * 在创建 PS的同时需要预编译SQL语句传入,并通过             * Connection 传递给数据库了,这时候数据库会将             * 进行计划生成,但是并不会执行该计划,因为语义             * 虽然确定了,但是还缺少具体的数据。             */            //2、创建PrepareStatement            PreparedStatement preparedStatement = connection.prepareStatement(sql);            //3、设置参数            preparedStatement.setString(1, username);            preparedStatement.setString(2, password);            //4、执行查询            resultSet = preparedStatement.executeQuery();            if (resultSet.next()) {                //取余额                int account = resultSet.getInt("account");                System.out.println("登录成功,账户余额为:"+account);            }else {                System.out.println("账号或密码错误!");            }        } catch (Exception e) {            e.printStackTrace();        }finally {            DBUtil.closeConnection(connection, statement, resultSet);        }    }}

测试的时候,发现再输入jack 1’ OR ‘1’=’1就会提示用户名或者密码错误。

使用Statement和PrepareStatement进行批量数据处理比较。

/* * 使用prepareStatement批量插入数据 */public class JDBCDemo3 {    public static void main(String[] args) {        Connection connection = null;        Statement statement = null;        try {            connection = DBUtil.getConnection();            //向userinfo中插入一万条数据            /**             * 使用Statement             */            /*statement=connection.createStatement();            System.out.println("开始插入:"+new Date());            for(int i=0;i<10000;i++){                String sql=                          "INSERT INTO userinfo "                          + "VALUES "                          + "(seq_userinfo_id.NEXTVAL,'"+i+"','1234','"+i+"','jdbc@qq.com')";                //每次执行,数据库都会生成一个执行计划!                statement.executeUpdate(sql);            }            System.out.println("插入完毕:"+new Date());*/            //花了5秒钟            /*             * 使用prepareStatement,不要像上面一样进行循环创建ps,超过200会出现递归 SQL 级别 1 出现错误             * ORA-01000: 超出打开游标的最大数,Oracle数据库中打开的游标最大数为             * 一定值,默认情况下是300,当代码到第二步时, 循环中一个PreparedStatement占用了一个数据库游标,执行的循环超过这个数时就会产生游标数目溢出错误             *第二步循环中每次执行完PreparedStatement,             *都将PreparedStatement.close()下,释放掉这个资源就好了             */            System.out.println("prepareStatement开始插入:"+new Date());            String sql1=                      "INSERT INTO userinfo "                      + "VALUES "                      + "(seq_userinfo_id.NEXTVAL,?,'1234',?,'jdbc@qq.com')";            //只会生成一个执行计划            PreparedStatement preparedStatement = connection.prepareStatement(sql1);            for(int i=0;i<10000;i++){                preparedStatement.setString(1, "scu"+i);                preparedStatement.setString(2, "100"+i);                //更新会自动提交事务,每次插入都提交会消耗时间                preparedStatement.executeUpdate();            }            System.out.println("prepareStatement插入完毕:"+new Date());//3秒钟        } catch (Exception e) {            e.printStackTrace();        }finally {            DBUtil.closeConnection();        }    }}

分别用Statement和PrepareStatement插入一万条数据。发现前者用了5秒后者用了3秒。所以在批量数据处理的时候PrepareStatement是能节约性能的。

元数据的获取
如果要获取查询结果的元数据,jdbc也是提供了相应的方法。如:查到多少个字段,字段叫什么名字等信息。

public class JDBCDemo6 {    public static void main(String[] args) {        Connection connection = null;        Statement statement = null;        ResultSet resultSet =null;        try {            connection=DBUtil.getConnection();            statement=connection.createStatement();            String sql="SELECT id,username FROM userinfo";            resultSet=statement.executeQuery(sql);            //获取结果集中元数据对象            ResultSetMetaData metaData = resultSet.getMetaData();            //获取查询字段数            int columnCount = metaData.getColumnCount();            System.out.println("查询的自段数为:"+columnCount);            //查看每个字段的名字,数据库下标从1开始            for(int i=1;i<=columnCount;i++){                String colName=metaData.getColumnName(i);                System.out.println(colName);            }        } catch (Exception e) {            e.printStackTrace();        }finally {            DBUtil.closeConnection();        }    }}
输出结果:查询的自段数为:2IDUSERNAME
原创粉丝点击