前二天,在struts介绍的实例中就提到数据库的访问使用了工厂模式,可以实现在线切换数据库的功能,同样是那个NetBeans工程,今天就来具体介绍我实现的思路。
网上有很多工厂模式的介绍,我最先接触是在.Net的PetShop项目里看到的,最近公司要搞Java,所以就搬到Java里来运用下,看了一些资料好象我这种实现的方法叫做简单工厂,是通过定义接口来实现的,在面向对象编程的世界里面,接口用来定义的一组规范,它强制规范实现类要一定要实现完成它的所有成员,至于接口的调用到底使用那个实现类则是在工厂类里面产生的。接口一般多定义对象的行为动作即方法,而抽象类则多用来定义对象的公共属性,比如男人和女人可以抽象出人做为抽象基类,因为都有人的一些公共特征,至于什么时候用接口什么时候用抽象类,这个就需要看实际项目中对象的关系了。
还是用户的增删改查功能的实现为例子,我们先需要定义用户的接口IUser,然后使用不同数据库来分别实现它,程序使用那种数据库则放在properties资源文件里配置,工厂在根据配置产生实例类,以接口形式返回到业务逻辑层,然后在页面里调用业务逻辑层方法。这里我用PowerDesiger简单画了一个类图如下:
首先我们来看一下IUser接口类的代码:
package dal;
import bean.User;
import java.util.List;
public interface IUser {
public int UserAdd(User user);
public int UserDel(int id);
public int UserUpdate(User user);
public User GetUserInfo(int id);
public List<User> GetUserList();
}
代码比较简单,就是定义用户业务逻辑所需要的全部方法,然后在子类里将会被一一实现。接口里增加一个方法,所有子类都必须要提供该方法的实现,不然会编译不过,这也是使用接口的一个规范问题。这里需要注意的是.Net里所有接口成员都不需要加访问修饰符,默认为public,而java则可以加public,如果接口里有字段属性则需要赋初始值,在调用的是以static final成员的形式来调用的。
我们再看一下DataAccess工厂代码:
/*
* 数据库访问工厂
*/
package dal;
/**
*
* @author Jonllen
* @create 2009-05-18 22:04:42
* @site http://www.jonllen.com
*/
public class DataAccess {
private DataAccess(){}
public static String daoPackageName = db.DbManager.GetConfig().getDefaultItem().getPackageName();
public static Object InstanceObject(String className)
{
String classPath = "dao." daoPackageName "." className;
try {
System.out.println("--实例化" classPath "类开始--");
return Class.forName(classPath).newInstance();
} catch (Exception ex) {
System.out.println("**实例化" classPath "类失败**");
ex.printStackTrace();
}
return null;
}
public static IUser CreateUser()
{
return (IUser)InstanceObject("UserDAO");
}
}
首先这个工厂类里面全是静态的成员,而且构造函数是private级别的,这其实是Java里单例的一种,为确保全局只存在一个实例。这里的daoPackageName是当前访问数据库类的包名,相当与.Net里的命名空间,我在程序里面做为一个约定,就是所有操作访问数据库的全部放在dao包下面,然后下面不同的数据库创建不同的包名,每个包下面的类名一样,这样我们根据不同数据库的包名就能创建下面类的事例,在.Net里我们使用Assembly.Load("命名空间").CreateInstance("类名全路径")程序集动态创建类的实例,而在Java里面使用Class.forName("类名全路径").newInstance(),再将Object类型强转为接口,因为这些类是实现了对应的业务接口的。在这里我推荐使用以上InstanceObject方法这种命名约定来动态创建类实例,而不要根据daoPackageName包名判断去new一个事例,如下代码是不妥的:
public static IUser CreateUser()
{
if (daoPackageName.equals("derby")) {
return (IUser)new dao.derby.UserDAO();
}else if(daoPackageName.equals("mysql")) {
return (IUser)new dao.mysql.UserDAO();
}else if(daoPackageName.equals("sqlserver")) {
return (IUser)new dao.sqlserver.UserDAO();
}
return null;
}
原因很简单,因为我都不知道到底将有多少像derby、mysql、sqlserver这样的实现类,所以使用daoPackageName判断去动态new对象是不合理的,这也违工厂模式设计的初衷,假如我现在新增一个oracle的实现类,那我岂不是又需要修改程序增加为oracle的判断,如果使用类全名动态实例则程序不需要修改任何源代码,只需要添加oracle实现类的引用,再设置daoPackageName为oracle即可。在实际应用中,完成好实现类,编译上传,接入到应用程序里,甚至可以不需要重启就能使用这个类。
通过使用接口,我们把功能的调用推迟给实例化的子类。不同的子类,实现的方式可能又不同,就像访问不同的数据库,sql语法可能存在差异。这里贴一下我在Java里面访问数据库的类,代码如下:
Java数据库访问辅助类 1/**//*
2 * 数据库访问辅助类
3 */
4package db;
5
6import java.sql.*;
7
8/** *//**
9 * @author Jonllen
10 * @create 2009-05-23 19:21:40
11 * @update 2009-10-03 15:34:50
12 */
13public class SqlHelper ...{
14
15 public static String ConnString = DbManager.GetConfig().getDefaultItem().getConnString();
16
17 public static String DriverClass = DbManager.GetConfig().getDefaultItem().getDriverClass();
18
19 //并发处理,确保线程是同步的
20 public static synchronized Connection GetConnection()
21 ...{
22 try ...{
23 Class.forName(DriverClass).newInstance();
24 Connection conn = null;
25 String user = DbManager.GetConfig().getDefaultItem().getUser();
26 String password = DbManager.GetConfig().getDefaultItem().getPassword();
27 if( user!=null && user.trim().length()!=0 && password!=null && password.trim().length()!=0)
28 ...{
29 conn = DriverManager.getConnection(ConnString,user,password);
30 }else
31 ...{
32 conn = DriverManager.getConnection(ConnString);
33 }
34 return conn;
35 } catch (Exception e) ...{
36 System.out.println("连接数据库失败:" ConnString);
37 e.printStackTrace();
38 }
39 return null;
40 }
41
42 public static ResultSet GetProcedureResultSet(String sql )
43 ...{
44 return GetProcedureResultSet(sql, null, null);
45 }
46
47 public static ResultSet GetProcedureResultSet(String sql, Object[][] inargs)
48 ...{
49 return GetProcedureResultSet(sql, inargs, null);
50 }
51
52 public static ResultSet GetProcedureResultSet(String sql, Object[][] inargs, Object[][] outargs)
53 ...{
54 Connection con = GetConnection();
55 CallableStatement cstmt = null;
56 try ...{
57 cstmt = con.prepareCall(sql);
58 if(inargs!=null && inargs.length>0)
59 ...{
60 //设置输入参数
61 for(int i=0;i<inargs.length;i )
62 ...{
63 cstmt.setObject(inargs[i][0].toString(),inargs[i][1]);
64 }
65 }
66 if (outargs!=null && outargs.length>0)
67 ...{
68 //如果有多个输出参数则注册
69 for(int i=0;i<outargs.length;i )
70 ...{
71 cstmt.registerOutParameter(outargs[i][0].toString(),Integer.valueOf(outargs[i][1].toString()));
72 }
73 }
74 Boolean b = cstmt.execute();
75 if (outargs!=null && outargs.length>0)
76 ...{
77 for(int i=0;i<outargs.length;i )
78 ...{
79 //把原来 输出参数 的类型 变成输出参数值 返回
80 outargs[i][1] = cstmt.getObject(outargs[i][0].toString());
81 }
82 }
83 if (b)
84 ...{
85 return cstmt.getResultSet();
86 }
87 } catch (SQLException e) ...{
88 // TODO Auto-generated catch block
89 e.printStackTrace();
90 }
91 return null;
92 }
93
94 public static ResultSet GetResultSet(String sql)
95 ...{
96 Connection con = GetConnection();
97 try ...{
98 //PreparedStatement pstmt = con.prepareStatement();
99 Statement stmt = con.createStatement();
100 return stmt.executeQuery(sql);
101 } catch (SQLException e) ...{
102 // TODO Auto-generated catch block
103 e.printStackTrace();
104 }
105 return null;
106 }
107
108 public static ResultSet GetResultSet(String sql, Object ... parms)
109 ...{
110 Connection con = GetConnection();
111 try ...{
112 PreparedStatement pstmt = con.prepareStatement(sql);
113 if(parms!=null && parms.length>0)
114 ...{
115 //设置输入参数
116 for(int i=0;i<parms.length;i )
117 ...{
118 pstmt.setObject(i 1,parms[i]);
119 }
120 }
121 boolean hasResutl = pstmt.execute();
122 if (hasResutl) return pstmt.getResultSet();
123 if(pstmt.getMoreResults() )...{
124 return pstmt.getResultSet();
125 }
126 System.out.println("UpdateCount:" pstmt.getUpdateCount());
127 } catch (SQLException e) ...{
128 // TODO Auto-generated catch block
129 e.printStackTrace();
130 }
131 return null;
132 }
133
134 public static Object ExcuteSclare(String sql, Object ... parms)
135 ...{
136 ResultSet rs = GetResultSet(sql, parms);
137 try ...{
138 if( rs!=null && rs.next())
139 ...{
140 return rs.getObject(1);
141 }else System.out.println("NULL");
142 } catch (SQLException e) ...{
143 // TODO Auto-generated catch block
144 e.printStackTrace();
145 }finally...{
146 CloseConnection( rs );
147 }
148 return null;
149 }
150
151 public static int ExecuteQuery(String sql, Object ... parms)
152 ...{
153 Connection con = GetConnection();
154 try ...{
155 PreparedStatement pstmt = con.prepareStatement(sql);
156 if(parms!=null && parms.length>0)
157 ...{
158 //设置输入参数
159 for(int i=0;i<parms.length;i )
160 ...{
161 pstmt.setObject(i 1,parms[i]);
162 }
163 }
164 return pstmt.executeUpdate();
165 } catch (SQLException e) ...{
166 // TODO Auto-generated catch block
167 e.printStackTrace();
168 }finally...{
169 CloseConnection(con);
170 }
171 return -1;
172 }
173
174 public static void CloseConnection(ResultSet rs)
175 ...{
176
177 try ...{
178 if (rs!=null && rs.getStatement()!=null && rs.getStatement().getConnection()!=null)
179 ...{
180 //rs.getStatement().close();
181 rs.getStatement().getConnection().close();
182 }
183 } catch (SQLException e) ...{
184 // TODO Auto-generated catch block
185 e.printStackTrace();
186 System.out.println("关闭连接时出错(From ResultSet)!" e.getMessage());
187 }
188 }
189
190 public static void CloseConnection(Connection con)
191 ...{
192 try ...{
193 if (con!=null && !con.isClosed())
194 ...{
195 con.close();
196 }
197 } catch (SQLException e) ...{
198 // TODO Auto-generated catch block
199 e.printStackTrace();
200 System.out.println("关闭连接时出错(From Connection)!" e.getMessage());
201 }
202 }
203
204
205}
其实,不同的数据库在细节上的差异还是有很多的,比如像自动增长主键的处理等。我这个事例中用了三种数据库:derby、mysql、sqlserver,由于derby是装NetBeans自带开源免费的数据库,以前从没有听说过,所以在插入用户的时候只能先查出最大编号做为主键插入,而在mysql里插入自动编号则显示的指定为default,sqlserver里自增主键列则不需要指定,如果是oracle的话还需要使用序列。另外不同的数据库函数使用也不一,如取当前时间access和mysql是使用now(),sqlserver里则是getdate(),oracle里用SYSDATE,这些都是我们需要注意的,说不定我们的项目那一天就需要移植到另一种数据库。
java里访问数据库都是使用java.sql.*下面的类,然后加载驱动文件。而.Net里则是提供了IConnection、IDataReader、IDbDataParameter等接口,并且为不同种数据库提供了专门的命名空间,提高访问效率。下面我整理java里访问sql2000、sql2005、mysql、derby数据库的连接字符串、驱动包类名表列出比较。
数据库 | 连接字符串 | 驱动包 |
sql2000 |
jdbc:microsoft:sqlserver://localhost;databaseName=master;user=sa;password=123 |
com.microsoft.jdbc.sqlserver.SQLServerDriver |
sql2005 |
jdbc:sqlserver://localhost\sql2005;databaseName=dbTest;user=sa;password=123 |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
mysql |
jdbc:mysql://localhost:3306/dbtest?user=root&password=123&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true |
com.mysql.jdbc.Driver |
derby |
jdbc:derby://localhost:1527/dbTest;create=true;user=app;password=app |
org.apache.derby.jdbc.ClientDriver |
oracle |
jdbc:oracle:thin:system/123:@localhost:1521:orcl |
oracle.jdbc.driver.OracleDriver |
这些信息是配置在我工程connector.properties资源文件里的,把数据库名做为key,然后对应connString连接字符串、driverClass驱动类多个属性,使用DbManager单件类读取到,以确保只读取一次资源文件,由于要做到在线切换数据库,而不是重起应用程序后才能生效,而之前DataAccess的daoPackageName、SqlHelper的ConnString和DriverClass静态字段只会在初始化读取一次,所以切换数据库的时候也重新指定这些静态字段,不然还会是初始化时候的值,代码如下:
public void setDefaultName(String defaultName) {
for(DbItem item : this.list)
{
if (item.getDbName().equalsIgnoreCase(defaultName))
{
System.out.println("修改设置当前数据库为:" defaultName );
this.defaultName = defaultName;
this.defaultItem = item;
dal.DataAccess.daoPackageName = item.getPackageName();
db.SqlHelper.ConnString = item.getConnString();
db.SqlHelper.DriverClass = item.getDriverClass();
}
}
}
这样切换数据库的功能就实现了,所有页面功能的调用是在BLL业务逻辑层,这一层就是使用接口实例调用方法。好了不多说了,有什么问题可以下载NetBeans工程的源代码,仍然是和上一篇struts同一工程。
Java工厂模式切换数据库实例源代码下载(带Struts的增、删、改、查功能)