Java-JDBC技术
发布日期:2021-06-29 12:34:08 浏览次数:4 分类:技术文章

本文共 31280 字,大约阅读时间需要 104 分钟。

序言

JDBC全称是Java DataBase Connectivity,即Java数据库连接,它是一种可以执行SQL语句的Java API。

这里写图片描述
Java API是接口,其实现类由各数据库厂商提供实现,这些实现类就是“驱动程序”。

1、JDBC基础编程

下面是JDBC编程步骤,以MySQL为例:

(1) 查询操作

Connection conn = null;    Statement stat = null;    ResultSet rs = null;    try {        //加载驱动            Class.forName("com.mysql.jdbc.Driver");        //使用driverManager获取数据库链接,获取connection代表了java程序和数据库的链接        conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/lcma", "root", "iflytek");        //使用conncetion创建一个statement对象        stat = conn.createStatement();        //执行sql语句        rs = stat.executeQuery("select * from student");        //不断使用next()将指针下移一行        while(rs.next()){            System.out.println(rs.getString("name"));        }    } 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();}} }

(2) 增删改查操作

Connection conn = null;Statement stat = null;try {    //加载驱动器    Class.forName("com.mysql.jdbc.Driver");    //通过driverManager获取数据库链接    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/lcma", "root", "iflytek");    //使用Connection创建一个Statement    stat = conn.createStatement();    //执行SQL(insert/update/delete)语句,返回数据库影响的行数    int rows = stat.executeUpdate("insert into student (id,name,age,cla_id) values (2,'马小超',22,1)");    if(rows == 1){        System.out.println("插入成功");    }} 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();}}}

(3) JDBC封装

通过对前面代码的分析,会发现以下几个问题:

1、Url、User、Password直接在代码中定义,如果数据库服务器稍作变动,怎么办?2、一个项目基本针对一个底层数据库,难道每次操作数据库,都要注册一次驱动程序嘛?是否可以只注册一次?3、获取数据库连接时,每次都需要Url、User、Password,一旦改动其中一个数据,意味着要修改所有此处的代码。4、释放资源,每次数据库操作后,都需要释放资源,难道每次操作后都要写三次try close catch代码嘛?

如果要解决上面的几个问题,那么就要对刚才的代码实现封装,并且把数据库的配置放到配置文件(Properties)中,具体做法如下:

新建jdbc.properties,

这里写图片描述
内容如下:

/*数据库连接配置*/    driverClassName=com.mysql.jdbc.Driver    url=jdbc:mysql://127.0.0.1:3306/lcma?characterEncoding=utf-8    user=root    password=123456

新建一个JdbcUtil类,实现代码如下:

import java.io.InputStream;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.Properties;public class JdbcUtil {
private static String url; private static String user; private static String password; /** *加载配置文件的时候使用了静态代码块,表明类一加载,配置文件就会立马加载 */ static{ //使用properties加载属性文件 Properties prop = new Properties(); try { InputStream is = JdbcUtil.class.getClassLoader().getResourceAsStream("com/iflytek/jdbc.properties"); prop.load(is); //注册驱动(获取属性文件中的数据) String driverClassName = prop.getProperty("driverClassName"); Class.forName(driverClassName); //获取属性文件中的url,username,password url = prop.getProperty("url"); user = prop.getProperty("user"); password = prop.getProperty("password"); } catch (Exception e) { e.printStackTrace(); } } //获取数据库连接 public static Connection getConnection(){ Connection conn = null; try { conn = DriverManager.getConnection(url, user, password); } catch (SQLException e) { e.printStackTrace(); } return conn; } //释放资源 public static void close(Connection conn, Statement stat, ResultSet rs){ if(conn != null){ try {conn.close();} catch (SQLException e) {e.printStackTrace();} } if(stat != null){ try {stat.close();} catch (SQLException e) {e.printStackTrace();} } if(rs != null){ try {rs.close();} catch (SQLException e) {e.printStackTrace();} } }}

在加载配置文件的时候使用了静态代码块,表明类一加载,配置文件就会立马加载,属性被保存在静态变量中(url,user,password)。

获取数据库连接和释放资源都采用了静态方法,通过类直接调用改方法,实现了公用代码的封装,降低的代码的耦合性。

这时候我们再来看JDBC获取数据库数据代码:

Connection conn = null;Statement stat = null;ResultSet rs = null;try{    //通过JdbcUtil获取数据库链接    conn = JdbcUtil.getConnection();    stat = conn.createStatement();    rs = stat.executeQuery("select * from student");    while(rs.next()){         System.out.println(rs.getString("name"));    }}catch(Exception e){    e.printStackTrace();}finally {    //通过JdbcUtil关闭资源    JdbcUtil.close(conn, stat, rs);}

通过现在的代码可以看出,代码简介了很多,没有出现容易出错的配置,获取连接,加载配置文件,关闭资源我们也不需要关心,大大降低了代码的耦合性,提高了代码重用性。

(4) PreparedStatement

封装过后的代码还是有些问题,就是采用了Statement对数据库操作,如果现在SQL语句需要传入变量,只有采用拼接的方式,这样做有很多缺点,例如拼接容易出错或容易产生SQL注入等。

通过PreparedStatement对数据库操作可以解决Statement带来的缺点,PreparedStatement和Statement区别如下:

Statement的缺点:

1、同样的SQL语句,每次都要发送,不能进行有效的缓存。2、拼接SQL字符串非常容易出现错误。3、不能防止恶意数据,易产生SQL注入。

升级后的新接口PreparedStatement(推荐):

1、预编译SQL语句,并进行有效的缓存,性能更好。2、允许使用问号占位符参数,并且该参数必须获得值后才可以执行。3、无需拼接SQL语句。

问号占位符参数:INSERTINTO User(id,name,age,birthday)VALUES(?,?,?,?);

来看一下通过PreparedStatement对数据库操作的代码:

Connection conn = null;PreparedStatement stat = null;ResultSet rs = null;try{        //通过JdbcUtil获取数据库链接    conn = JdbcUtil.getConnection();    stat = conn.prepareStatement("select * from student where name like ? and age = ? ");    stat.setString(1, "%小%");    stat.setInt(2, 22);    rs = stat.executeQuery();    while(rs.next()){           System.out.println(rs.getString("name"));    }    }catch(Exception e){           e.printStackTrace();    }finally {        //通过JdbcUtil关闭资源        JdbcUtil.close(conn, stat, rs);    }}

注意以下两点:

1、问号占位符不能加引号。2、占位符参数设置值从下标从1开始。

2、JDBC分层实现

我们这里就采用分层操作:Dao层,Service层(Java Web中常用)。

首先看一下domain域中的User实体:

package com.weijia.domain;import java.util.Date;public class User {
private int id; private String name; private Date birthday; private float money; public User(){ } public User(int id,String name,Date birthday,float money){ this.id = id; this.name = name; this.birthday = birthday; this.money = money; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public float getMoney() { return money; } public void setMoney(float money) { this.money = money; } @Override public String toString(){ return "[id="+id+",name="+name+",birthday="+birthday+",money="+money+"]"; }}

再来看一下Dao层结构:

接口:

package com.weijia.domain;    public interface UserDao {        //添加用户        public void addUser(User user);        //通过userid查询用户,id是唯一的,所以返回的是一个user        public User getUserById(int userId);        //更新用户信息        public int update(User user);        //删除用户信息        public int delete(User user);    }

实现类:

package com.weijia.domain;import java.sql.Connection;import java.sql.Date;import java.sql.PreparedStatement;import java.sql.ResultSet;import com.weijia.firstdemo.JdbcUtils;public class UserDaoImpl implements UserDao{
/** * 添加用户 */ public void addUser(User user) { Connection conn = null; PreparedStatement st = null; try{ conn = JdbcUtils.getConnection(); String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)"; st = conn.prepareStatement(sql); st.setInt(1,user.getId()); st.setString(2,user.getName()); //日期格式的转换(utils.date转化成sql.date) st.setDate(3,new Date(user.getBirthday().getTime())); st.setFloat(4, user.getMoney()); int count = st.executeUpdate(); System.out.println("添加记录条数:"+count); }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(null, st, conn); } } /** * 删除用户 */ public int delete(User user) { Connection conn = null; PreparedStatement st = null; try{ conn = JdbcUtils.getConnection(); String sql = "delete from user where id=?"; st = conn.prepareStatement(sql); st.setInt(1,user.getId()); int count = -1; count = st.executeUpdate(); System.out.println("删除记录条数:"+count); return count; }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(null, st, conn); } } /** * 通过userId获取用户信息 */ public User getUserById(int userId) { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); String sql = "select * from user where id=?"; st = conn.prepareStatement(sql); st.setInt(1,userId); rs = st.executeQuery(); if(rs.next()){ User user = new User(); user.setId(userId); user.setName(rs.getString("name")); user.setBirthday(rs.getDate("birthday")); user.setMoney(rs.getFloat("money")); return user; } }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(rs, st, conn); } return null; } /** * 更新用户信息 */ public int update(User user){ Connection conn = null; PreparedStatement st = null; try{ conn = JdbcUtils.getConnection(); String sql = "update user set name=?,birthday=?,money=? where id=?"; st = conn.prepareStatement(sql); st.setString(1,user.getName()); st.setDate(2,new Date(user.getBirthday().getTime())); st.setFloat(3,user.getMoney()); st.setInt(3,user.getId()); int count = 0; count = st.executeUpdate(); System.out.println("更新的记录数:"+count); return count; }catch(Exception e){ throw new DaoException(e.getMessage(),e); }finally{ JdbcUtils.free(null, st, conn); } }}

然后是Servic层:

package com.weijia.domain;public class UserService {
private UserDao userDao; public UserService(){ //通过工厂实例化UserDao对象 userDao = DaoFactory.getInstance().createUserDao(); System.out.println("userDao:"+userDao); } /** * 注册用户 * @param user */ public void regist(User user){ if(user == null){ System.out.println("注册信息无效!!"); }else{ userDao.addUser(user); } } /** * 查询用户 * @param userId * @return */ public User query(int userId){ User user = userDao.getUserById(userId); if(user == null){ System.out.println("查询结果为空!!"); }else{ System.out.println(user.getId()+"\t"+user.getName()+"\t"+user.getBirthday()+"\t"+user.getMoney()); } return userDao.getUserById(userId); } /** * 更新用户 * @param user */ public void update(User user){ if(user.getId()<=0){ System.out.println("用户id无效,无法更新"); }else{ userDao.update(user); } } /** * 删除用户 * @param user */ public void delete(User user){ if(user.getId()<=0){ System.out.println("用户id无效,无法删除!!"); }else{ userDao.delete(user); } }}

这里我们还需要额外的两个类:

一个是异常类,因为我们需要自定义我们自己的一个异常,这样方便进行捕获:

package com.weijia.domain;public class DaoException extends RuntimeException{
private static final long serialVersionUID = 1L; public DaoException(){ } public DaoException(Exception e){ super(e); } public DaoException(String msg){ super(msg); } public DaoException(String msg,Exception e){ super(msg,e); }}

同时,我们这里面采用工厂模式进行实例化UserDao对象:

package com.weijia.domain;import java.io.FileInputStream;import java.util.Properties;public class DaoFactory {
/** * 单例模式 */ private static UserDao userDao = null; private static DaoFactory instance = new DaoFactory(); private DaoFactory(){ /** * 通过读取属性文件来动态的加载Dao层类 */ Properties prop = new Properties(); try{ FileInputStream fis = new FileInputStream("src/com/weijia/domain/daoconfig.properties"); prop.load(fis); String className = prop.getProperty("userDaoClass"); Class
clazz = Class.forName(className); userDao = (UserDao)clazz.newInstance(); fis.close(); }catch(Throwable e){ throw new ExceptionInInitializerError(e); } } public static DaoFactory getInstance(){ return instance; } public UserDao createUserDao(){ return userDao; }}

这里面是读取properties文件,然后去读取类名进行加载,这种方式是很灵活的

测试代码:

package com.weijia.domain;import java.util.Date;public class TestDemo {
public static void main(String[] args) throws Exception{ UserService userService = new UserService(); System.out.println("添加用户:"); userService.regist(new User(1,"jiangwei",new Date(System.currentTimeMillis()),300)); }}

这里我们看到其实这些操作真的很简单,就是按照那样的几个步骤来操作即可,同时我们还需要将结构进行分层,以便管理,我们这里面测试的时候,撇开了创建数据库的一个环节,至于那个环节,也是不难的,可以从网上搜索一下即可。


3、Statement与SQL注入

接着来看一下关于我们上面的例子中使用了Statement进行操作的,其实这里面是存在一个问题的,就是会有sql注入的问题,我们先来看一下这个问题:

查询学生信息:

/**     * 使用Statement读取数据     * @param name     * @throws SQLException     */    static void read(String name) throws SQLException{        Connection conn = null;        Statement st = null;        ResultSet rs = null;        try {            conn = JdbcUtils.getConnection();            //创建语句            st = conn.createStatement();            //执行语句(不建议使用*)            String sql = "select id,name from user where name='"+name+"'";            rs = st.executeQuery(sql);            //根据列名取数据            while(rs.next()){                System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t");            }        }catch(SQLException e){            e.printStackTrace();        }catch(Exception e){            e.printStackTrace();        }finally{            JdbcUtils.free(rs, st, conn);        }    }

我们使用代码测试一下:

read("'or 1 or'");

我们运行会发现,将查询出所有的学生的记录,这个是什么原因呢?我们不妨将sql打印一下会发现:

select id,name from user where name=''or 1 or''

擦,因为sql语句中把1认为是true,又因为是或的关系,所以将所有的学生的信息查询出来了,这个就是sql注入,因为Statement会把传递进来的参数进行一下转化操作,用引号包含一下,所以会出现这个问题,那么我们该怎么解决呢?

有的同学说我们可以添加一句过滤的代码,将传递的参数取出单引号,这个方法是可行的的,但是这个只能解决那些使用单引号的数据库,可能有的数据库使用的是双引号包含内容,那就不行了,所以应该想一个全套的方法,那么这里我们就是用一个叫做:PreparedStatement类,这个类是Statement类的子类,上文有介绍。

我们将上面读取用户信息的代码改写成PreparedStatement:

/**     * 使用PreparedStatement     * @param name     * @throws SQLException     */    static void readPrepared(String name) throws SQLException{        Connection conn = null;        PreparedStatement st = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            //执行语句(不建议使用*)            String sql = "select id,name from user where name=?";            //创建语句            st = conn.prepareStatement(sql);            st.setString(1, name);            rs = st.executeQuery();            //根据列名取数据            while(rs.next()){                System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t");            }        }catch(Exception e){        }    }

之后我们在执行:

readPrepared("'or 1 or'");

就不会全部查出来了,只会查询空结果,因为表中没有一个学生的名字叫做 ‘or 1 or’。


4、JDBC中事务的概念

我们当初在学习数据库的时候就了解事务的概念了,事务在数据库中的地位是很重要的。在JDBC中默认情况事务是自动提交的,所以我们在进行CRUD操作的时候不需要关心开启事务,提交事务,事务回滚的一些操作,那么下面我们就来看一下怎么手动的操作一些事务:

下载我们假定这样的一个场景:

有来两个用户1和2,现在:1、将用户1中的账户的钱减少102、查询用户2中的账户的钱,如果钱少于300,就增加10,否则抛出异常

看一下代码:

static void test() throws Exception{        Connection conn = null;        Statement st = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            /**************事务START********************/            conn.setAutoCommit(false);            st = conn.createStatement();            String sql = "update user set money=money-10 where id=1";            st.executeUpdate(sql);            sql = "select money from user where id=2";            rs = st.executeQuery(sql);            float money = 0.0f;            if(rs.next()){                money = rs.getFloat("money");            }            if(money>300){                throw new RuntimeException("已经超过最大值");            }            sql = "update user set money=money+10 where id=2";            st.executeUpdate(sql);            conn.commit();            /*******************事务END*********************/        }catch(RuntimeException e){        }finally{            JdbcUtils.free(rs, st, conn);        }    }

我们运行测试一下,因为我们这里想让它抛出异常,所以我们将用户2中的钱改成大于300的,运行一下,结果抛出异常了,但是我们发现了用户1中的钱少了10,但是由于抛出异常,所以后面的代码不执行了,用户2中的钱没有变化,那么这样的操作明显不对的,所以我们这时候要解决这个问题,使用事务的回滚操作,在捕获到异常的时候需要做回滚操作:

if(conn != null){       conn.rollback();    }

这样即使抛出了异常,这些操作也会进行回滚的,那么用户1中的钱就不会少10了。


5、JDBC中调用存储过程

我们现在想在插入一条数据的时候能够得到主键id的值(因为我们一般把主键id的值设置成自增长的形式),首先来创建一个存储过程:

delimiter $$ //修改定界符drop procedure if exists addUser $$create procedure addUser(in name varchar(45),in birthday date,in money float,out pid int)begin insert into user(name,birthday,money) values(anme,birthday,money); select last_insert_id() into pid;//当前线程拿到最后一次插入的记录的赋值给pid,这里要注意,user表中的id必须是主键自增长类型,不然报错end $$delimiter ;

这里,name,birthday,money都是输入值是:in,pid是输出值:out。

然后我们在代码中进行执行这个存储过程:

static void test() throws Exception{        Connection conn = null;        CallableStatement cs = null;        try{            conn = JdbcUtils.getConnection();            //name,birthday,money,id            //存储过程名称是:addUser            String sql = "{ call addUser(?,?,?,?)}";            cs = conn.prepareCall(sql);            cs.registerOutParameter(4, Types.INTEGER);            cs.setString(1,"jiangwei");            cs.setDate(2,new Date(System.currentTimeMillis()));            cs.setFloat(3,300);            cs.executeUpdate();            int id = cs.getInt(4);            System.out.println("id:"+id);            /**             * 通过这个存储过程来获取主键id是有一个问题,不同的数据库,存储过程的编写语法是不一样的,所以这种方法是不通用             * 还有另外一种方法是OtherApi,通过JDBC中的api来获取             */        }catch(SQLException e){            e.printStackTrace();        }    }

这样我们就得到了插入一条记录的时候得到他的主键id的值

其实这种调用存储结构的方法,在早起的时候是很实用的,因为那时候没有分层架构的思想,所以会将业务逻辑层的实现放到存储过程中去做了,在调用存储过程的时候,会发现一个问题就是这样去获取主键id的值的方式,是不通用的,因为不同的数据库可能存储过程的编写是不一样的,所以做不到一致性,而且现在有了三成架构的思想,我们慢慢的就将这种方式给淘汰了,而是直接使用JDBC给我们提供的一套api来获取主键key的值:直接上代码吧:

static void test() throws Exception{        java.sql.Connection conn = null;        PreparedStatement ps = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            String sql = "insert into user(name,birthday,money) values('jiangwei','1987-01-01',400)";            ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);//生成主键id            ps.executeUpdate();            //可能是组合主键,可能会返回一个ResultSet            rs = ps.getGeneratedKeys();            int id = 0;            if(rs.next()){                id = rs.getInt(1);            }            System.out.println("id:"+id);        }catch(Exception e){            e.printStackTrace();        }finally{            JdbcUtils.free(rs,ps,conn);        }    }

我们只要设置一个参数Statement.RETURN_GENERATED_KEYS就可以得到一个主键集合了,这里要注意的是,因为有的表结构中会出现组合主键的情况,所以返回的是一个主键集合。这种方式就和底层数据库摆脱了关系,做到一致性了。


6、JDBC实现批处理

在前面的例子中会发现,每次都是执行一条语句,然后关闭连接,这样效率可能会很低,如果我们想一次插入几千条数据的话,这时候可以使用批处理的功能,所谓批处理就是将多个执行语句进行捆绑然后去执行,但是效率上并非就一定高,因为我们知道这个数据库连接是tcp的,所以在将多个语句捆绑在一起的时候,在传输的过程中也是会进行分包发送的,这个包的大小也不是固定的,这个大小很难掌控的,我们之后经过多次测试之后,才能得到一次批量处理的适宜数量。下面来看一下实例吧:

首先是普通的插入一条数据:

static void create() throws Exception{        //建立一个连接的是很耗时间的        //执行一个sql语句也是很耗时间的        //优化的措施:批处理        Connection conn = null;        PreparedStatement ps = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            String sql = "insert user(name,birthday,money) values(?,?,?)";            ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);            ps.setString(1,"jiangwei");            ps.setDate(2,new Date(System.currentTimeMillis()));            ps.setFloat(3,400);            ps.executeUpdate();        }catch(Exception e){            e.printStackTrace();        }finally{            JdbcUtils.free(rs, ps, conn);        }    }

然后是批处理插入100条数据:

static void createBatch() throws Exception{        //建立一个连接的是很耗时间的        //执行一个sql语句也是很耗时间的        //优化的措施:批处理        Connection conn = null;        PreparedStatement ps = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            String sql = "insert user(name,birthday,money) values(?,?,?)";            ps = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);            //打包的话容量也不是越大越好,因为可能会内存溢出的,同时网络传输的过程中也是会进行拆包传输的,这个包的大小是不一定的            //有时候打包的效率不一定就会高,这个和数据库的类型,版本都有关系的,所以我们在实践的过程中需要检验的            for(int i=0;i<100;i++){                ps.setString(1,"jiangwei");                ps.setDate(2,new Date(System.currentTimeMillis()));                ps.setFloat(3,400);                //ps.addBatch(sql);                ps.addBatch();            }            ps.executeBatch();        }catch(Exception e){            e.printStackTrace();        }finally{            JdbcUtils.free(rs, ps, conn);        }    }

测试代码:

public static void main(String[] args) throws Exception{        long startTime = System.currentTimeMillis();        for(int i=0;i<100;i++){            create();        }        long endTime = System.currentTimeMillis();        System.out.println("For Waste Time:"+(endTime-startTime));        createBatch();        System.out.println("Batch Waste Time:"+(System.currentTimeMillis()-endTime));    }

我们在控制台中看到他们分别消耗的时间:

这里写图片描述
我们可以看到这个批处理消耗的时间明显很少。。当然我们在开始的时候也说过了,这个批处理的最适宜的大小要掌控好。


7、JDBC中CRUD的模板模式

我们从前面的例子中可以看到,我们在操作CRUD的时候,返现有很多重复的代码,比如现在一个UserDao来操作查询操作,写了一段查询代码,然后有一个ProductDao也来操作查询操作,也写了一段查询代码,其实我们会发现这两个查询代码中有很多是重复的,这时候我们就想了,能不能够进行代码的优化,我们想到了模板模式,就是将相同的代码提取出来放到父类中做,不同的代码放到各自的子类中去做,这样重复的代码只会出现一次了。

下面来看一下实例,首先我们看一下抽象出来的Dao代码:

package com.weijia.template;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import com.weijia.domain.DaoException;import com.weijia.firstdemo.JdbcUtils;public abstract class AbstractDao {
/** * 更新 */ protected int update(String sql,Object[] args) { //这里需要做判断的,可能args为null Connection conn = null; PreparedStatement st = null; try{ conn = JdbcUtils.getConnection(); st = conn.prepareStatement(sql); for(int i=0;i

看一下UserDaoImpl类:

package com.weijia.template;import java.sql.ResultSet;import java.sql.SQLException;import com.weijia.domain.User;public class UserDaoImpl extends AbstractDao{
/** * 更新用户信息 */ public int update(User user) { String sql = "udpate user set name=?,birthday=?,money=?,where id=?"; Object[] args = new Object[]{user.getName(),user.getBirthday(),user.getMoney(),user.getId()}; return super.update(sql, args);//相同的代码调用父类的方法即可 } /** * 删除用户 * @param user */ public void delete(User user){ String sql = "delete from user where id=?"; Object[] args = new Object[]{user.getId()}; super.update(sql, args); } /** * 查找用户 * @param loginName * @param password * @return */ public User findUser(String loginName){ String sql = "select id,name,money,birthday from user where name=?"; Object[] args = new Object[]{loginName}; return (User)super.find(sql, args); } @Override protected Object rowMapper(ResultSet rs) throws SQLException{ User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setMoney(rs.getFloat("money")); user.setBirthday(rs.getDate("birthday")); return user; } //如果insert的时候不需要获取主键的话,也可以使用super.update方法实现的,这样代码就显得很整洁,相同的代码只需要一份即可(放在父类中) //不同的地方放到子类来实现 //首先要区分哪些是变动的部分,哪些是不变的部分即可}

ProductDaoImpl类:

package com.weijia.template;import java.sql.ResultSet;public class ProductDaoImpl extends AbstractDao{
public int update(){ String sql = "update product set pname=?,price=? where pid=?"; Object[] args = new Object[]{
"drug",11,1}; return super.update(sql, args); } @Override protected Object rowMapper(ResultSet rs) { return null; }}

看到了,这样来实现的话,代码就很简洁了,这里的ProductDaoImpl类中没有写完,就是大概是那个意思。这里体现出了一个设计模式就是:模板模式

接着看,现在有一个问题,就是查询,其实update的方式很简单的,完全可以统一化的,因为查询需要处理查询之后的结果集,所以很纠结的,上面的例子中我们看到,我们查询的是一个User对象,假如现在我只是想查询一个用户的name,那么我们只能在写一个findUserName方法了,同时还需要在AbstractDao父类中添加一个抽象方法的行映射器,这种方式就很纠结了,假如我们还有其他的查询需要的话,重复的代码又开始多了,这里我们将采用 策略模式 进行解决,我们只需要定义行映射器的接口:

package com.weijia.strategy;import java.sql.ResultSet;import java.sql.SQLException;public interface RowMapper {
public Object mapRow(ResultSet rs) throws SQLException;}

在父类中只需要修改一下查询的方法:

/**     * 查找用户     * @param sql     * @param args     * @param rowMapper     * @return     */    protected Object find(String sql,Object[] args,RowMapper rowMapper){        Connection conn = null;        PreparedStatement st = null;        ResultSet rs = null;        try{            conn = JdbcUtils.getConnection();            st = conn.prepareStatement(sql);            for(int i=0;i

添加了一个RowMapper接口变量

然后在子类中实现这个接口即可:

/**     * 查询名称     * @param id     * @return     */    public String findUserName(int id){        String sql = "select name from user where id=?";        Object[] args = new Object[]{id};        Object user = super.find(sql, args,new RowMapper(){            public Object mapRow(ResultSet rs) throws SQLException {                return rs.getObject("name");            }        });        return ((User)user).getName();    }    /**     * 采用策略模式:传递不同的行为:C++中可以使用函数指针来实现,Java中可以使用接口的回调来实现     * @param loginName     * @param password     * @return     */    public User findUser(String loginName){        String sql = "select id,name,money,birthday from user where name=?";        Object[] args = new Object[]{loginName};        return (User)super.find(sql, args,new RowMapper(){            public Object mapRow(ResultSet rs) throws SQLException {                User user = new User();                user.setId(rs.getInt("id"));                user.setName(rs.getString("name"));                user.setMoney(rs.getFloat("money"));                user.setBirthday(rs.getDate("birthday"));                return user;            }        });    }

我们可以看到这两个查询的方法就很优雅了,这样做了之后,我们只需要在指定的子类中添加指定的方法即可,其实策略模式很简单的,就是相当于回调机制,就是想执行指定的方法,但是Java中没有函数指针,C++中其实可以的,所以只能通过回调来实现了。

通过上面的CRUD优化之后,我们在进行操作的时候,代码编写是很方便和简洁的。


8、Spring框架中的JdbcTemplate

说完了上面的我们自定义的CRUD模板,下面我来看一下Spring框架给我们提供的CRUD模板(JdbcTemplate),其实他的实现原理和我们上面是一样的,只是他的功能会更强。

下面来看一下实例代码:

package com.weijia.springtemplate;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.List;import java.util.Map;import org.springframework.dao.DataAccessException;import org.springframework.jdbc.core.BeanPropertyRowMapper;import org.springframework.jdbc.core.ConnectionCallback;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.core.RowMapper;import com.weijia.domain.User;import com.weijia.firstdemo.JdbcUtils;public class JdbcTemplateTest {
public static void main(String[] args){ User user = new User(); user.setMoney(20); user.setId(1); update(user); } /** * 更新操作 * @param user */ static void update(User user){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "update user set money=? where id=?"; Object[] args = new Object[]{user.getMoney(),user.getId()}; jdbc.update(sql, args); } /** * 通过用户名查询用户 * @param name * @return */ static User findUser(String name){ //需要传递一个数据源 JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where name=?"; Object[] args = new Object[]{name}; //queryForObject方法和我们之前采用策略模式设置的模板很类似呀,这个方法只会返回一个记录,如果有多个记录返回或者没有记录返回的话,这个方法就会报告异常的 Object user = jdbc.queryForObject(sql,args,new RowMapper(){ public Object mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setMoney(rs.getFloat("money")); user.setBirthday(rs.getDate("birthday")); return user; }}); return (User)user; } /** * 通过用户名查询实体类 * @param name * @return */ static User findUsers(String name){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where name=?"; Object[] args = new Object[]{name}; //如果没有记录或者返回多个记录的话,这个方法是会报异常的 //使用这个方法直接将返回的结果集映射到实体类,这里返回的结果集中的字段和实体类中的属性名必须相等 //如果不相等的话,就是用默认值对其属性进行赋值 Object user = jdbc.queryForObject(sql,args,new BeanPropertyRowMapper(User.class){}); return (User)user; } /** * 查询多个用户 * @param id * @return */ static List findUser1(int id){ JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource()); String sql = "select id,name,money,birthday from user where id

下面来看一下,JdbcTemplate的相关使用方法:

首先看一下,我们开始使用一个数据源来初始化一个JdbcTemplate模板:

JdbcTemplate jdbc = new JdbcTemplate(JdbcUtils.getDataSource());

然后只需要直接执行其相关的方法即可:

1、update(String sql,Object[] args):

第一个参数是sql语句,第二参数是需要填充的更新参数
2、queryForObject(String sql,Object[] args, RowMapper rowMapper):
第一参数是sql语句,第二参数是需要填充的查询参数,第三个参数是行映射器(和前面我们设计的一样),这个方法只适用查询结果是一个的情况,如果查询结果是多个的话,这个方法会报异常的,同时这个方法第三个参数我们也可以传递一个:new BeanPropertyRowMapper(User.class){}对象,这个就可以将查询结果填充到User实体类中了,当然这里有一个限制就是要求查询出来的结果集中的字段名和实体类中的属性名一样,其实这内部使用的是反射技术来实现的,我们之前写过这样的方法的。
3、query(String sql,Object[]args,RowMapper rowMapper):
这个方法和上面的那个方法不同的就是返回的结果,这个方法返回的是一个List,针对于查询结果是多个的情况
4、queryForInt(String sql,Object[] args):
这个方法是针对查询结果是一个整型的,比如我们需要查询出用户的总数。
5、queryForLong(String sql,Object[] args):这个方法是查询出long类型的。
6、queryForObject(String sql, Class requiredType):
这个方法是对于那些没有特定查询类型的方法同一使用这个方法,比如现在想查询一个用户的名称是String类型的,或者想查询用户的money,是float类型的,这里我们只需要在第二个参数中指定类型即可。
7、queryForMap(String sql,Object[] args):查询返回的是一个Map集合
8、queryForList(String sql,Object[] args):查询返回的是一个List集合

上面的方法我们就足够用了。

下面再来看一个需求,如果我们想得到插入一条记录之后的主键的操作,这里改如何操作呢?

在之前我们操作的是需要将PreparedStatement中设置一个参数,不然会报错的,我们通过上面的方法可以知道其内部都是使用PreparedStatement实现的,因为有占位符,需要设置查询参数的。但是他并没有提供一个方法能够设置这个PreparedStatement的一些参数,但是如果我们想获取到主键值的话,必须要设置PreparedStatement的第二参数为:Statement.RETURN_GENERATED_KEYS,那么这时候,JdbcTemplate还给我们提供了一个方法:

jdbc.execute(new ConnectionCallback(){    public Object doInConnection(Connection con) throws SQLException,DataAccessException {        //do something        return null;    }});

execute方法中传递一个Connection的回调类,然后实现一个方法,在方法中我们可以获取到当前的连接,当我们拿到了这个连接的话,就可以进行操作了。

这样一来,上面提到的JdbcTemplate中的方法就可以满足我们的日常需求了。


转载地址:https://bwshen.blog.csdn.net/article/details/82316023 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java-数组/集合工具类
下一篇:Java-基础语法合集

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2024年04月05日 22时31分06秒