最近想实现用户自定义数据库中的字段,我想大部分人第一想到的就是EAV(Entity-Attribute-Value),这种方式对于写一个小的毕业设计应该还可以使用,当然也有很多CMS系统采用这种方式,毕竟其中Value表中的数据会猛增,同样,会涉及到查询优化问题,暂不考虑。

其次,在J2EE中,如果使用spring+hbiernate+springMVC(struts2),Entity类有两种方式和数据库进行映射,一种是注解方式,一种是*.hbm.xml配置文件方式。

①注解方式,对于注解方式,因为最终目的是根据自定义的字段可以实时的在数据库表中对其字段进行生成,然后可以使用,最初想到的解决方法是重写相应实体类的.class文件,然后根据最新的.class文件生成相应的*.hbm.xml文件,利用configuration重新读取*.hbm.xml文件来建立buildSessionFactory(下面会给出详细代码),最后发现即使实现了增加字段,也无法通过这种方式删除字段,不过还是看看往.class文件中写入field以及其getter/setter方法的java语句吧。

1 /*2 * 添加字段
3*/
4 public class AddFieldAdapter extendsClassAdapter {5
6 private intaccessModifier;7 privateString name;8 privateString desc;9 private booleanisFieldPresent;10
11 public AddFieldAdapter(ClassVisitor cv, intaccessModifier, String name,12String desc) {13 super(cv);14 this.accessModifier =accessModifier;15 this.name =name;16 this.desc =desc;17}18
19 public FieldVisitor visitField(intaccess, String name, String desc,20String signature, Object value) {21 if (name.equals(this.name)) {22 isFieldPresent = true;23}24 returncv.visitField(access, name, desc, signature, value);25}26
27 public voidvisitEnd() {28 if (!isFieldPresent) {29 FieldVisitor fv = cv.visitField(accessModifier, name, desc, null,30 null);31
32 if (null !=fv) {33fv.visitEnd();34}35}36cv.visitEnd();37}39 }
1 //创建get,public,无参数,有返回值
2 MethodVisitor mv = cWriter.visitMethod(Opcodes.ACC_PUBLIC, "get"
3 + StringUtils.capitalize(filedName), "()" + type, null,//type为返回的类型
4 null);5mv.visitCode();6 mv.visitVarInsn(Opcodes.ALOAD, 0);//将this压栈
7mv.visitFieldInsn(Opcodes.GETFIELD,8 this.entityClass.getSimpleName(), filedName, type);9mv.visitInsn(Opcodes.ARETURN);10 mv.visitMaxs(1, 1);11mv.visitEnd();12
13 //创建set方法,public,传递一个参数
14 mv = cWriter.visitMethod(Opcodes.ACC_PUBLIC,//方法名为public
15 "set" + StringUtils.capitalize(filedName), "(" + type //传递一个参数
16 + ")V", null, null);//V表示返回的是void
17 mv.visitCode();//开始执行
18 mv.visitVarInsn(Opcodes.ALOAD, 0);//将this压栈
19 mv.visitVarInsn(Opcodes.ALOAD, 1);//将局部变量压栈
20mv.visitFieldInsn(Opcodes.PUTFIELD,21 this.entityClass.getSimpleName(), filedName, type);22mv.visitInsn(Opcodes.ARETURN);23 mv.visitMaxs(2, 2);24 mv.visitEnd();//执行结束

关于如何将.class文件生成*.hbm.xml可以从网上找相关模板,借助模板将class文件转换生成xml配置文件方式,因为重点不采用这种方式,所以简单介绍下如何重写class文件即可,关于如何从*.hbm.xml文件重构sessionfactory映射到数据库中在稍后贴出代码。

②配置文件方式,配置文件的方式对于项目中使用本身就不是特别方便的(相对于注解来说),所有这种方式也是一开始只是为了尝试在重写了*.hbm.xml后是否可以通过代码的方式,在不重启服务器的情况下,将修改的内容实时的反映到我们的数据库中,所以在一开始的时候定义model或者entity时候,就应该写个与之对应的*.hbm.xml,例如下面这样。

1 /**
2 *实体类3 */
4 public class Contact extendsCustomizableEntity {5
6 /**
7 * ID8 */
9 private intid;10
11 /**
12 * 名称13 */
14 privateString name;15
16 public intgetId() {17 returnid;18 }19
20 publicString getName() {21 returnname;22 }23
24 public void setId(intid) {25 this.id =id;26 }27
28 public voidsetName(String name) {29 this.name =name;30 }31 }
1 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 
3 default-cascade="none"default-lazy="true">
4
5 
6 mutable="true"name="com.hfmx.model.Contact"optimistic-lock="version"
7 polymorphism="implicit"select-before-update="false"table="tb_contact">
8 
9 
10 
11
12 
13 name="name"optimistic-lock="true"type="string"unique="false" />
14 
15 optimistic-lock="true"unique="false"update="true">
16 
17 
18

上面的实体类中继承了一个父类,以及XML文件中的标签dynamic-component都是为了可以自定义字段做准备的,我们暂且忽略这部分内容,主要看看如何重构sessionFactory,其实这个实现的方式也是从网上找到的,当然为了能够与hibernate4.0结合,部分地方作了修改,下面把代码贴出来,重点地方会做点注释。

1 public classHibernateUtil {2
3 private staticHibernateUtil instance;4
5 privateConfiguration configuration;6
7 privateSessionFactory sessionFactory;8
9 privateSession session;10
11 public synchronized staticHibernateUtil getInstance() {12 if (null ==instance) {13 instance = newHibernateUtil();14 }15 returninstance;16 }17
18 private synchronizedSessionFactory getSessionFactory() {19 if (null ==sessionFactory) {20 Configuration _configuration = this.getConfiguration();21
22 ServiceRegistry serviceRegistry = newServiceRegistryBuilder()23 .applySettings(_configuration.getProperties())24 .buildServiceRegistry();25
26 sessionFactory =_configuration27 .buildSessionFactory(serviceRegistry);28 }29 returnsessionFactory;30 }31
32 public synchronizedSession getCurrentSession() {33 if (null ==session) {34 session =getSessionFactory().openSession();35 session.setFlushMode(FlushMode.COMMIT);36 }37 returnsession;38 }39
40 private synchronizedConfiguration getConfiguration() {41 if (null ==configuration) {42 try{43 //默认加载hibernate.cfg.xml
44 configuration = newConfiguration().configure();45 Class entityClass = Contact.class;46 String filePath =entityClass.getResource(47 entityClass.getSimpleName() + ".hbm.xml").getPath();48 //替换空格
49 filePath = filePath.replace("%20", " ");50
51 File file = newFile(filePath);52 //通过class加载会发现在j2ee中不行53 //configuration.addClass(entityClass);54 //通过加载classes文件夹下面的文件,获取实时修改后的XML文件
55 configuration.addFile(file);56 } catch(HibernateException e) {57 e.printStackTrace();58 }59 }60 returnconfiguration;61 }62
63 /**
64 * 重置65 */
66 public voidreset() {67 Session session =getCurrentSession();68 if (null !=session) {69 session.flush();70 if(session.isOpen()) {71 session.close();72 }73 }74 SessionFactory sf =getSessionFactory();75 if (null !=sf) {76 sf.close();77 }78 this.configuration = null;79 this.sessionFactory = null;80 this.session = null;81 }82
83 publicPersistentClass getClassMapping(Class entityClass) {84 returngetConfiguration().getClassMapping(entityClass.getName());85 }86 }
这种方式即可在修改完XML后立即加载修改后的XML,对其进行

映射到数据库中,然后生成新添加的字段,这种方式存在的弊端也在文章一开始做了介绍,无法删除字段,同时,配置文件在实际项目中越来越多的被Annotation所替代掉。所以,需要有一种新的方式来实现自定义字段,下面将重点介绍这种方式,同时,下面的内容也是对此配置方式的一个补充。如果看到这里觉得配置文件方式介绍的不够详细,可以在下面找出其中很多知识点。

现在,着重讲解通过纯的sql来实现数据库的自定义字段功能….

首先,我们来看下环境,我用的是springMVC+spring3+hibernate4.0

同时,对应新增的字段,要通过key-value的方式进行保存,即放在Map集合中,这样方便后期的读写

1 /**
2 * 支持自定义字段的业务实体类基类3 *4 *@authorwy5 *6 */
7 public abstract classCustomizableEntity {8
9 private MapcustomProperties;10
11 public MapgetCustomProperties() {12 if (null ==customProperties)13 customProperties = new HashMap();14 returncustomProperties;15 }16
17 public void setCustomProperties(MapcustomProperties) {18 this.customProperties =customProperties;19 }20
21 publicObject getValueOfCustomField(String name) {22 returngetCustomProperties().get(name);23 }24
25 public voidsetValueOfCustomField(String name, Object value) {26 getCustomProperties().put(name, value);27 }28 }
1 /**
2 * 实体类MyUser继承CustomizableEntity,这个类里面已存在字段可视为固定字段3 */
4 @Entity5 public class MyUser extendsCustomizableEntity {6
7 /**
8 * ID9 */
10 private intid;11
12 /**
13 * 姓名14 */
15 privateString userName;16
17 @Id18 @GeneratedValue19 public intgetId() {20 returnid;21 }22
23 publicString getUserName() {24 returnuserName;25 }26
27 public void setId(intid) {28 this.setValueOfCustomField("id", id);29 this.id =id;30 }31
32 public voidsetUserName(String userName) {33 this.setValueOfCustomField("username", userName);34 this.userName =userName;35 }36 }
完成上面两个类,在启动项目后,数据库中会添加一个myuser表,同时拥有两个字段(ID,userName)。
此时,我们来尝试看,看可否往myuser表中添加一个字段,通过下面简单的方式。
1 /**
2 * 添加字段列(UserDefineField是一个简单的实体类,包括自定义字段的name,*type以及中文名称,描述等等)3 */
4 public voidaddFieldColumn(Class clazz, UserDefineField userDefine) {5 try{6 Session session = this.sessionFactory.getCurrentSession();7
8 String sql = "";9 //验证字段是否已经存在
10 sql = "select count(*) as c from userdefinefield where tableName='"
11 +clazz.getCanonicalName()12 + "' and fieldName='"
13 + userDefine.getFieldName() + "'";14 Query countQuery =session.createSQLQuery(sql);15 List list =countQuery.list();16 long count = 0;17 if (list.size() > 0) {18 Object object = list.get(0);19 if (null !=object) {20 count =Long.parseLong(object.toString());21 }22 }23 System.out.println("count:" +count);24
25 if (count <= 0) {26 //字段不存在是添加字段
27 sql = "alter table " + clazz.getSimpleName() + " add column "
28 + userDefine.getFieldName() + " "
29 +userDefine.getFieldType();30 Query query =session.createSQLQuery(sql);31 query.executeUpdate();32
33 //修改自定义字段表
34 sql = "insert into userdefinefield (tableName,fieldName,fieldType,fieldCN,fieldDesc) values('"
35 +clazz.getCanonicalName()36 + "','"
37 +userDefine.getFieldName()38 + "','"
39 +userDefine.getFieldType()40 + "','"
41 +userDefine.getFieldCN()42 + "','"
43 + userDefine.getFieldDesc() + "')";44 query =session.createSQLQuery(sql);45 query.executeUpdate();46 }47 } catch(Exception e) {48 System.out.println("纯数据库方式动态添加字段名称失败");49 e.printStackTrace();50 }51 }
执行完后我们会发现数据库表中确实已经增加了一个新的字段,那对于删除字段,当然也就类似了,简单的看下代码
1 /**
2 * 删除字段列3 *4 *@paramclazz5 *@paramfieldName6 */
7 public voiddelFieldColumn(Class clazz, String fieldName) {8 Session session = this.sessionFactory.getCurrentSession();9 try{10 //删除字段
11 String sql = "alter table " +clazz.getSimpleName()12 + " drop column " + fieldName + "";13 Query query =session.createSQLQuery(sql);14 query.executeUpdate();15
16 sql = "delete from userdefinefield where tableName='"
17 + clazz.getCanonicalName() + "' and fieldName='"
18 + fieldName + "'";19 query =session.createSQLQuery(sql);20 query.executeUpdate();21
22 } catch(Exception e) {23 System.out.println("纯数据库方式动态删除字段名称失败");24 e.printStackTrace();25 }26 }
看到这里,很多人会不会想,这样实现只要会sql的人都可以,主要是如何对数据进行CRUD的操作,是的,光看上面的代码肯定觉得so easy,接下来看看如果查询、保存、修改、删除吧
① 查询
1 /**
2 * 根据ID进行用户查询3 *4 *@paramclazz5 *@paramid6 *@return
7 */
8 public MyUser searchMyUser(Class clazz, intid) {9 MyUser myUser = newMyUser();10 try{11 Session session = this.sessionFactory.getCurrentSession();12 String sql = "SELECT * FROM (select * from myuser where id="
13 +id14 + " ) m join (select * from userdefinefield where tableName='"
15 + clazz.getCanonicalName() + "') AS define";16
17 Query query =session.createSQLQuery(sql).setResultTransformer(18 Transformers.ALIAS_TO_ENTITY_MAP);19 List> list = (List>) query20 .list();21
22 if (list.size() > 0) {23 Map map = list.get(0);24
25 //固有属性
26 myUser.setId(Integer.parseInt(map.get("id").toString()));27 myUser.setUserName(map.get("userName").toString());28 }29
30 for (Mapmap : list) {31
32 //自定义列名
33 String fieldName = map.get("fieldName").toString();34
35 myUser.setValueOfCustomField(fieldName, (null ==map36 .get(fieldName)) ? "": map.get(fieldName).toString());37
38 UserDefine define = newUserDefine(map);39 myUser.setValueOfExctalyCustomProp(fieldName, define);40 }41
42 returnmyUser;43 } catch(Exception e) {44 System.out.println("根据ID进行查询出现错误");45 e.printStackTrace();46 return null;47 }48 }
查询后,在control中打印出来看看效果
1 //****查询信息
2 MyUser myUser = this.myservice.searchMyUser(MyUser.class, id);3
4 System.out.println("******查询结果******");5 System.out.println("id:" +myUser.getId());6 System.out.println("name:" +myUser.getUserName());
//自定义字段key-value显示7 for(String key : myUser.getCustomPropties.keySet()) {8 System.out.println("" + key + ":"+key+” value:” +myUser.getValueOfCustomField(key));10 }
② 保存
1 /**
2 * 保存信息3 *4 *@parammyuser5 */
6 public voidsaveMyUser(MyUser myuser) {7 try{8 Session session = this.sessionFactory.getCurrentSession();9
10 String sql = "insert into " +myuser.getClass().getSimpleName()11 + " ";12 int index = 0;13 for(String key : myuser.getCustomProperties().keySet()) {14 if (index == 0) {15 sql += "(" + key + "";16 } else{17 if (index == myuser.getCustomProperties().size() - 1) {18 sql += "," + key + ") ";19 } else{20 sql += "," + key + "";21 }22 }23 index++;24 }25
26 index = 0;27 for(String key : myuser.getCustomProperties().keySet()) {28 if (index == 0) {29 sql += "values('" +myuser.getCustomProperties().get(key)30 + "'";31 } else{32 if (index == myuser.getCustomProperties().size() - 1) {33 sql += ",'" +myuser.getCustomProperties().get(key)34 + "')";35 } else{36 sql += ",'" +myuser.getCustomProperties().get(key)37 + "'";38 }39 }40 index++;41 }42
43 System.out.println("保存用户信息的sql:" +sql);44 Query query =session.createSQLQuery(sql);45 query.executeUpdate();46
47 } catch(Exception e) {48 System.out.println("保存用户信息出错:" +e.getMessage());49 e.printStackTrace();50 }51 }
③  修改
1 /**
2 * 修改信息3 *4 *@parammyUser5 */
6 public voidupdateMyUser(MyUser myUser) {7 try{8 Session session = this.sessionFactory.getCurrentSession();9
10 String sql = "update myuser set userName='" +myUser.getUserName()11 + "'";12 if (myUser.getCustomProperties().size() > 0) {13 for(String key : myUser.getCustomProperties().keySet()) {14 sql += "," + key + "='"
15 + myUser.getCustomProperties().get(key) + "'";16 }17 }18 sql += " where id=" +myUser.getId();19
20 System.out.println("修改用户信息的sql:" +sql);21
22 Query query =session.createSQLQuery(sql);23 query.executeUpdate();24
25 } catch(Exception e) {26 System.out.println("修改用户信息出错:" +e.getMessage());27 e.printStackTrace();28 }29 }
④ 删除

删除如果是根据ID删除,那就一点影响都没有了,如果是根据动态列内容去删除,那也就是和保存和修改时候处理方式一样,这里就不列出来了。(哈哈,感觉重复的代码有点多了)

综上所述,初步觉得,如果采用这种方式应该可以对项目本身改动的地方不大,同时,表结构基本上能够很好的维护。在多表联合查询的时候,也同样没有太复杂的sql代码。当然,目前没有运用到大的系统中,不知道可否在后面会遇到问题。

如果其他童鞋有其他更好的方法,希望可以一起分享!!!!!!