11.2 解决方案
11.2.1 代理模式来解决
用来解决上述问题的一个合理的解决方案就是代理模式。那么什么是代理模式呢?
(1)代理模式定义
(2)应用代理模式来解决的思路
仔细分析上面的问题,一次性访问多条数据,这个可能性是很难避免的,是客户的需要。也就是说,要想节省内存,就不能从减少数据条数入手了,那就只能从减少每条数据的数据量上来考虑。
一个基本的思路如下:由于客户访问这多条用户数据的时候,基本上只需要看到用户的姓名,因此可以考虑刚开始从数据库查询返回的用户数据就只有用户编号和用户姓名,当客户想要详细查看某个用户的数据的时候,再次根据用户编号到数据库中获取完整的用户数据。这样一来,就可以在满足客户功能的前提下,大大减少对内存的消耗,只是每次需要重新查询一下数据库,算是一个以时间换空间的策略。
可是该如何来表示这个只有用户编号和姓名的对象呢?它还需要实现在必要的时候访问数据库去重新获取完整的用户数据。
代理模式引入一个Proxy对象来解决这个问题,刚开始只有用户编号和姓名的时候,不是一个完整的用户对象,而是一个代理对象,当需要访问完整的用户数据的时候,代理会从数据库中重新获取相应的数据,通常情况下是当客户需要访问除了用户编号和姓名之外的数据的时候,代理才会重新去获取数据。
11.2.2 模式结构和说明
代理模式的结构如图11.1所示:
图11.1 代理模式的结构示意图
Proxy:
代理对象,通常具有如下功能:
- 实现与具体的目标对象一样的接口,这样就可以使用代理来代替具体的目标对象
- 保存一个指向具体目标对象的引用,可以在需要的时候调用具体的目标对象
- 可以控制对具体目标对象的访问,并可能负责创建和删除它
Subject:
目标接口,定义代理和具体目标对象的接口,这样就可以在任何使用具体目标对象的地方使用代理对象
RealSubject:
具体的目标对象,真正实现目标接口要求的功能。
在运行时刻一种可能的代理结构的对象图如图11.2所示:
图11.2 运行时刻一种可能的代理结构的对象图
11.2.3 代理模式示例代码
(1)先看看目标接口的定义,示例代码如下:
/** * 抽象的目标接口,定义具体的目标对象和代理公用的接口 */ public interface Subject { /** * 示意方法:一个抽象的请求方法 */ public void request(); } |
(2)接下来看看具体目标对象的实现示意,示例代码如下:
/** * 具体的目标对象,是真正被代理的对象 */ public class RealSubject implements Subject{ public void request() { //执行具体的功能处理 } } |
(3)接下来看看代理对象的实现示意,示例代码如下:
/** * 代理对象 */ public class Proxy implements Subject{ /** * 持有被代理的具体的目标对象 */ private RealSubject realSubject=null; /** * 构造方法,传入被代理的具体的目标对象 * @param realSubject 被代理的具体的目标对象 */ public Proxy(RealSubject realSubject){ this.realSubject = realSubject; } public void request() { //在转调具体的目标对象前,可以执行一些功能处理
//转调具体的目标对象的方法 realSubject.request();
//在转调具体的目标对象后,可以执行一些功能处理 } } |
11.2.4 使用代理模式重写示例
要使用代理模式来重写示例,首先就需要为用户对象定义一个接口,然后实现相应的用户对象的代理,这样在使用用户对象的地方,就使用这个代理对象就可以了。
这个代理对象,在起初创建的时候,只需要装载用户编号和姓名这两个基本的数据,然后在客户需要访问除这两个属性外的数据的时候,才再次从数据库中查询并装载数据,从而达到节省内存的目的,因为如果用户不去访问详细的数据,那么那些数据就不需要被装载,那么对内存的消耗就会减少。
先看看这个时候系统的整体结构,如图11.3所示:
图11.3 代理模式重写示例的系统结构示意图
此时的UserManager类,充当了标准代理模式中的Client的角色,因为是它在使用代理对象和用户数据对象的接口。
还是看看具体的代码示例,会更清楚。
(1)先看看新定义的用户数据对象的接口,非常简单,就是对用户数据对象属性操作的getter/setter方法,因此也没有必要去注释了,示例代码如下:
/** * 定义用户数据对象的接口 */ public interface UserModelApi { public String getUserId(); public void setUserId(String userId); public String getName(); public void setName(String name); public String getDepId(); public void setDepId(String depId); public String getSex(); public void setSex(String sex); } |
(2)定义了接口,需要让UserModel来实现它。基本没有什么变化,只是要实现这个新的接口而已,就不去代码示例了。
(3)接下来看看新加入的代理对象的实现,示例代码如下:
/** * 代理对象,代理用户数据对象 */ public class Proxy implements UserModelApi{ /** * 持有被代理的具体的目标对象 */ private UserModel realSubject=null;
/** * 构造方法,传入被代理的具体的目标对象 * @param realSubject 被代理的具体的目标对象 */ public Proxy(UserModel realSubject){ this.realSubject = realSubject; } /** * 标示是否已经重新装载过数据了 */ private boolean loaded = false;
public String getUserId() { return realSubject.getUserId(); } public void setUserId(String userId) { realSubject.setUserId(userId); } public String getName() { return realSubject.getName(); } public void setName(String name) { realSubject.setName(name); } public void setDepId(String depId) { realSubject.setDepId(depId); } public void setSex(String sex) { realSubject.setSex(sex); } public String getDepId() { //需要判断是否已经装载过了 if(!this.loaded){ //从数据库中重新装载 reload(); //设置重新装载的标志为true this.loaded = true; } return realSubject.getDepId(); } public String getSex() { if(!this.loaded){ reload(); this.loaded = true; } return realSubject.getSex(); } /** * 重新查询数据库以获取完整的用户数据 */ private void reload(){ System.out.println("重新查询数据库获取完整的用户数据,userId==" +realSubject.getUserId()); Connection conn = null; try{ conn = this.getConnection(); String sql = "select * from tbl_user where userId=?";
PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, realSubject.getUserId()); ResultSet rs = pstmt.executeQuery(); if(rs.next()){ //只需要重新获取除了userId和name外的数据 realSubject.setDepId(rs.getString("depId")); realSubject.setSex(rs.getString("sex")); }
rs.close(); pstmt.close(); }catch(Exception err){ err.printStackTrace(); }finally{ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public String toString(){ return "userId="+getUserId()+",name="+getName() +",depId="+getDepId()+",sex="+getSex()+"\n"; } private Connection getConnection() throws Exception { Class.forName("你用的数据库对应的JDBC驱动类"); return DriverManager.getConnection( "连接数据库的URL", "用户名", "密码"); } } |
(3)看看此时UserManager的变化,大致如下:
- 从数据库查询值的时候,不需要全部获取了,只需要查询用户编号和姓名的数据就可以了
- 把数据库中获取的值转变成对象的时候,创建的对象不再是UserModel,而是代理对象,而且设置值的时候,也不是全部都设置,只是设置用户编号和姓名两个属性的值
示例代码如下:
/** * 实现示例要求的功能 */ public class UserManager { /** * 根据部门编号来获取该部门下的所有人员 * @param depId 部门编号 * @return 该部门下的所有人员 */ public Collection<UserModelApi> getUserByDepId( String depId)throws Exception{ Collection<UserModelApi> col = new ArrayList<UserModelApi>(); Connection conn = null; try{ conn = this.getConnection(); //只需要查询userId和name两个值就可以了 String sql = "select u.userId,u.name " +"from tbl_user u,tbl_dep d " +"where u.depId=d.depId and d.depId like ?";
PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, depId+"%");
ResultSet rs = pstmt.executeQuery(); while(rs.next()){ //这里是创建的代理对象,而不是直接创建UserModel的对象 Proxy proxy = new Proxy(new UserModel()); //只是设置userId和name两个值就可以了 proxy.setUserId(rs.getString("userId")); proxy.setName(rs.getString("name"));
col.add(proxy); }
rs.close(); pstmt.close(); }finally{ conn.close(); } return col; } private Connection getConnection() throws Exception { Class.forName("你用的数据库对应的JDBC驱动类"); return DriverManager.getConnection( "连接数据库的URL", "用户名", "密码"); } } |
(4)写个客户端来测试看看,是否能正确实现代理的功能呢,示例代码如下:
public class Client { public static void main(String[] args) throws Exception{ UserManager userManager = new UserManager(); Collection<UserModelApi> col = userManager.getUserByDepId("0101");
//如果只是显示用户名称,那么不需要重新查询数据库 for(UserModelApi umApi : col){ System.out.println("用户编号:="+umApi.getUserId() +",用户姓名:="+umApi.getName()); } //如果访问非用户编号和用户姓名外的属性,那就会重新查询数据库 for(UserModelApi umApi : col){ System.out.println("用户编号:="+umApi.getUserId() +",用户姓名:="+umApi.getName() +",所属部门:="+umApi.getDepId()); } } } |
运行结果如下:
用户编号:=user0001,用户姓名:=张三1 用户编号:=user0002,用户姓名:=张三2 用户编号:=user0003,用户姓名:=张三3 重新查询数据库获取完整的用户数据,userId==user0001 用户编号:=user0001,用户姓名:=张三1,所属部门:=010101 重新查询数据库获取完整的用户数据,userId==user0002 用户编号:=user0002,用户姓名:=张三2,所属部门:=010101 重新查询数据库获取完整的用户数据,userId==user0003 用户编号:=user0003,用户姓名:=张三3,所属部门:=010102 |
仔细查看上面的结果数据会发现,如果只是访问用户编号和用户姓名的数据,是不需要重新查询数据库的,只有当访问到这两个数据以外的数据时,才需要重新查询数据库以获得完整的数据。这样一来,如果客户不访问除这两个数据以外的数据,那么就不需要重新查询数据库,也就不需要装载那么多数据,从而节省内存。
(5)1+N次查询
看完上面的示例,可能有些朋友会发现,这种实现方式有一个潜在的问题,就是如果客户对每条用户数据都要求查看详细的数据的话,那么总的查询数据库的次数会是1+N次之多。
第一次查询,获取到N条数据的用户编号和姓名,然后展示给客户看。如果这个时候,客户对每条数据都点击查看详细信息的话,那么每一条数据都需要重新查询数据库,那么最后总的查询数据库的次数就是1+N次了。
从上面的分析可以看出,这种做法最合适的场景就是:客户大多数情况下只需要查看用户编号和姓名,而少量的数据需要查看详细数据。这样既节省了内存,又减少了操作数据库的次数。
看到这里,可能会有朋友想起,Hibernate这类ORM的框架,在Lazy Load的情况下,也存在1+N次查询的情况,原因就在于,Hibernate的Lazy Load就是使用代理来实现的,具体的实现细节这里就不去讨论了,但是原理是一样的。