12. 利用反射机制完成对象关系映射
反射机制实现简单的半自动化ORM(Object Retional Mapping)
“半自动化”的ibatis,却刚好解决了这个问题。这里的“半自动化”,是相对Hibernate等提供了全面的数据库封装机制的“全自动化”。ORM 实现而言,“全自动”ORM 实现了 POJO 和数据库表之间的映射,以及 SQL 的自动生成和执行。而ibatis 的着力点,则在于POJO 与 SQL之间的映射关系。也就是说,ibatis 并不会为程序员在运行期自动生成 SQL 执行。具体的 SQL 需要程序员编写,然后通过映射配置文件,将SQL所需的参数,以及返回的结果字段映射到指定 POJO。
摘自:【百度百科 词条:ibatis】
完成的功能:
a. 数据表到POJO的映射,POJO属性名与数据表字段名可不一致。当数据表字段名发生修改时,无须通过硬编码方式修改POJO代码,修改配置文件POJO属性到数据表的字段映射即可完成,达到松耦合目的。
b. 数据库SQL语句放在配置文件中,由相关接口调用,某些情况下,数据库操作发生变化时,能够很方便的修改SQL语句。感觉有点类似于将SQL语句写到数据库存储过程中。
c. 所有SQL语句放在一起便于集中管理,增强代码复用性、易于修改、维护。例如:某条业务逻辑相关的SQL语句在多处有使用到,传统情况下需要在将SQL语句内嵌到程序代码中多次,每次的书写导致繁琐,复用性差。另外,当业务逻辑变更时,相应的SQL语句需要做一定的修改,此时内嵌程序代码中的SQL语句又会出现维护难题,不易修改、维护。
d. 相对全自动化来说,半自动化更加灵活。程序员能根据需要方便地修改SQL语句。
实现过程:
1. 自定义数据源配置文件data-source-config.xml
简单提出一些约束条件(对于完成的框架被称作文档类型定义dtd)。这些约束在解析该数据源配置文件时用到。
ORM配置文件标签类型说明:
<sqlMap>对象关系映射根标签
<data-source>数据源配置文件地址
<resultMap>对象关系映射:用于将数据表的字段映射到对象属性
<result>单条对象关系映射
<statement>增查改删(CRUD)SQL语句
标签属性说明:
<resultClass>标签属性: id, class
<statement>标签属性: id, parameterClass, resultClass
<result>标签属性: property(对象属性), column(数据表字段)
配置ORM几点约束:
1. 有且只有一个<sqlMap>标签
2. 标签拼写大小写敏感,目前最多三级标签
3. 出现多个 id 的标签,以第一个为准
4. 不同的标签可以用相同的 id 属性值
5. SQL语句必须用#包裹
6. <statement>,<resultMap>标签必须用 id,resultClass 属性
7. 没有参数的可以不设置 parameterClass 属性
8. parameterClass,resultClass 属性,对于基本类型,要求输入其包装类完全限定名称
parameterClass,resultClass支持类型
1. 所有基本数据类型java.lang.Integer,java.lang.Long,java.lang.Float
2. 复合类型java.lang.String,java.util.Map,java.util.List,java.util.Date
3. 自定义映射类型
<?xml version="1.0" encoding="UTF-8"?>
<!-- quote from sql-map-config.xml of ibatis 2.0 -->
<sqlMap>
<data-source config="com/iteye/jarg/resources/data-source-config.xml" />
<!--
映射表字段column与Bean属性property
-->
<resultMap id="User2" class="com.iteye.jarg.test.User2">
<result property="id" column="id" />
<result property="name" column="username" />
<result property="pwd" column="password" />
</resultMap>
<!-- 用户自定义类型 -->
<statement id="query" parameterClass="com.iteye.jarg.test.User2" resultClass="User2">
select *
from user
where id = #id# and username = #name# and password = #pwd#
</statement>
<!-- 以下返回单条记录 -->
<statement id="queryUserById" parameterClass="java.lang.Integer" resultClass="com.iteye.jarg.test.User">
select *
from user
where id = #id#
</statement>
<!-- 参数可以使用复合类型,只取出需要的,对应的数据 -->
<statement id="queryUserById2" parameterClass="java.util.Map" resultClass="User2">
select *
from user
where id = #id#
</statement>
...
<statement id="queryUserWithList" parameterClass="java.util.List" resultClass="com.iteye.jarg.test.User">
select *
from user
where username = #username# and password = #password#
</statement>
<!-- 以下返回多条记录 -->
<statement id="queryUsersById" parameterClass="java.lang.Integer" resultClass="com.iteye.jarg.test.User">
select *
from user
where id != #id#
</statement>
<statement id="queryUsersWithMap" parameterClass="java.util.Map" resultClass="User2">
select *
from user
where username != #username# or password != #password#
</statement>
...
<statement id="queryAllUser" resultClass="com.iteye.jarg.test.User">
select *
from user
</statement>
</sqlMap>
2. 定义程序中用到的相关常量
这些常量大部分是在解析上面的xml文档时用到。由于基本类型在反射时需要用到它们的包装类生成 Class 类实例,所以配置文件中属性 parameterClass,resultClass 要求输入基本类型的完全限定名称。因此,对于基本类型的数据类型包含包装类型字符串和基本类型字符串,包装类型字符串在前,基本类型字符串在后,用空格隔开。映射类型用com.iteye.jarg.test.ResultMap类来表示。类型集为了方便类型匹配,找出对应的参数类型。
关键代码:
/**
* Copyright (c) 2011 Trusted Software and Mobile Computing(TSMC)
* All right reserved.
*
* Created on Aug 20, 2011 4:00:36 PM
* http://jarg.iteye.com/
* Author: Jarg Yee <yeshaoting@gmail.com>
*/
/** 对象关系映射配置文件路径 */
private static final String CONFIG = "mapper-config.xml";
/** 配置文件根节点 */
private static Element root = null;
/** 配置文件节点名 */
private static final String NODE_DATA_SOURCE = "data-source";
private static final String NODE_TYPE_ALIAS = "typeAlias";
private static final String NODE_RESULT_MAP = "resultMap";
private static final String NODE_PARAMETER_MAP = "parameterMap";
private static final String NODE_PROPERTY = "property";
private static final String NODE_STATEMENT = "statement";
private static final String NODE_PARAMETER = "parameter";
private static final String NODE_RESULT = "result";
/** 配置文件节点属性名 */
private static final String ATT_CONFIG = "config";
private static final String ATT_NAME = "name";
private static final String ATT_VALUE = "value";
private static final String ATT_ID = "id";
private static final String ATT_CLASS = "class";
private static final String ATT_PROPERTY = "property";
private static final String ATT_COLUMN = "column";
private static final String ATT_PARAMETER_CLASS = "parameterClass";
private static final String ATT_PARAMETER_MAP = "parameterMap";
private static final String ATT_RESULT_CLASS = "resultClass";
private static final String ATT_RESULT_MAP = "resultMap";
/** 反射机制获取方法名前缀 */
private static final String PREFIX_SET = "set";
private static final String PREFIX_GET = "get";
/** 数据类型 */
private static final String TYPE_BYTE = "java.lang.Byte byte";
private static final String TYPE_BOOLEAN = "java.lang.Boolean boolean";
private static final String TYPE_CHAR = "java.lang.Character char";
private static final String TYPE_SHORT = "java.lang.Short short";
private static final String TYPE_INT = "java.lang.Integer int";
private static final String TYPE_LONG = "java.lang.Long long";
private static final String TYPE_FLOAT = "java.lang.Float float";
private static final String TYPE_DOUBLE = "java.lang.Double double";
private static final String TYPE_Date = "java.util.Date";
private static final String TYPE_STRING = "java.lang.String";
private static final String TYPE_OBJECT = "java.lang.Object";
private static final String TYPE_MAP = "java.util.Map";
private static final String TYPE_LIST = "java.util.List";
private static final String TYPE_PARAMETER_MAP = "com.iteye.jarg.test.ParameterMap";
private static final String TYPE_RESULT_MAP = "com.iteye.jarg.test.ResultMap";
/** 类型集 */
private static final String[] TYPE = {
TYPE_BYTE, TYPE_BOOLEAN, TYPE_CHAR, TYPE_SHORT, TYPE_INT, TYPE_LONG,
TYPE_FLOAT, TYPE_DOUBLE, TYPE_Date, TYPE_STRING, TYPE_OBJECT, TYPE_MAP,
TYPE_LIST, TYPE_PARAMETER_MAP, TYPE_RESULT_MAP
};
/** 获取数据库连接 */
private Connection conn = null;
3. 初始化预编译SQL语句
这一步是用来解析SQL语句标签<statement>,获取相应的预编译SQL语句编号,参数类型,结果集类型,SQL语句,放于SqlMapStatement实例中。
关键代码:
/**
* TODO SQL语句声明实体
*/
public class SqlMapStatement
{
private String sql;
private String id;
private Class<?> parameterClass;
private String resultClass;
...
}
4. 获取预编译SQL语句
将<statement>标签中的SQL语句转换成真正的预编译SQL语句。这一步就是将#号包裹的输入参数替换成?(预编译SQL语句中代表一个参数)。利用正则表达式#[0-9a-zA-Z]*#,将开始#username#,#password#等替换成?。
关键代码:
//利用正则表达式,将#包围的字符串规制成?,形成预编译的SQL语句形式.
String statement = sqlMapStatement.getSql();
String sql = statement.replaceAll("#[0-9a-zA-Z]*#", "?");
PreparedStatement pstmt = conn.prepareStatement(sql);
5. 获取预编译声明实例
5.1 获取参数个数
根据预编译SQL语句中问号(?)个数来确定。
关键代码:
/**
* 获出预编译SQL语句参数个数
* */
private int getParameterNum(String sql)
{
int count = 0;
for(int i=0; i<sql.length(); i++)
{
if(sql.charAt(i) == '?')
{
count = count + 1;
}
}
return count;
}
5.2 设置预编译SQL语句中的参数值
_ 多个参数情况
需要用到复合类型(java.util.Map,java.util.List)、自定义映射类型(如:上面配置文件提到的User2)或自定义类类型(如:上面配置文件提到的com.iteye.jarg.test.User)。根据原始预编译SQL语句(<statement>标签下的文本值)中由#包裹的参数名,从复合类型,自定义类类型中取出对应参数名的数值或根据参数出现先后顺序从 java.util.List 中依次取出数值。
最后,通过 java.sql.PreParedStatement.setObject(int index, Object obj) 方法设置预编译SQL语句第index个参数,这一方法会根据 obj 的原始类型完成相对应的设置。
void setObject(int parameterIndex,
Object x)
throws SQLException
使用给定对象设置指定参数的值。第二个参数必须是类型 Object;所以,应该对内置类型使用 java.lang 的等效对象。 JDBC 规范指定了一个从 Java Object 类型到 SQL 类型的标准映射关系。在发送到数据库之前,给定参数将被转换为相应 SQL 类型。
_ 一个参数情况
匹配数据类型集数据类型与SqlMapStatement实例中输入参数类型(<statement>标签属性parameterClass值)。
最后,通过 java.sql.PreParedStatement.setObject(int index, Object obj) 方法设置预编译SQL语句第index个参数,这一方法会根据 obj 的原始类型完成相对应的设置。
注:基本数据类型(如:java.lang.Integer),需要通过反射机制将输入参数转换成对应的基本数据类型。例如:输入参数类型为java.lang.Integer参数,需要先调用java.lang.Integer类的parseInt(String obj)方法,返回对应类型的数值,再调用 java.sql.PreParedStatement.setObject(int index, Object obj) 方法。
另外,一个参数的参数类型若不是简单数据类型而复合数据类型或自定义映射类型的话,会通过从复合数据类型或自定映射类型中取出合适的数据作为该参数值填充。如:取出 java.util.Map 或 自定义类类型 中对应参数名的数值;取出 java.util.List 的第一个数值等。
5.3 返回 PreparedStatement 实例
该实例到此已经完成预编译SQL语句中参数设置。
关键代码:
//count=0时不需要设置参数
int count = getParameterNum(sql); //通过获取sql语句中的 ? 数目确定参数个数
if(count != 0)
{
/**
* 输入参数填充预编译SQL语句
*
* parameterClass,resultClass支持类型
* 1. 简单数据类型java.lang.Integer,java.lang.Long,java.lang.Float,java.lang.String等
* 2. 复合类型java.util.Map,java.util.List
* 3. 自定义映射类型,自定义类类型
* */
if(count == 1)
{
for(int i=0; i<11; i++)
{
if(sqlMapStatement.getParameterClass().getName().trim().equals(TYPE[i].split(" ")[0].trim()))
{
int size = TYPE[i].split(" ").length; //是否包含基本类型包装类 与 基本类型
Class<?> clazz = Class.forName(TYPE[i].split(" ")[0]);
if(size==2)
{
//还得将参数值转换成对应的基本类型
Method method = clazz.getMethod(getMethodName("parse",TYPE[i].split(" ")[1]), String.class);
Object obj = method.invoke(clazz, paramObject.toString());
pstmt.setObject(1, obj);
return pstmt;
}
if(size==1)
{
pstmt.setObject(1, paramObject.toString());
return pstmt;
}
}
}
}
/**
* 按顺序取出值作为输入参数 - List
* */
if(sqlMapStatement.getParameterClass().getName().equals(TYPE_LIST))
{
List list = (List)paramObject;
for(int i=0; i<count; i++)
{
pstmt.setObject(i+1, list.get(i));
}
return pstmt;
}
/**
* 按关键字取出值作为输入参数 - Map
* */
else if(sqlMapStatement.getParameterClass().getName().equals(TYPE_MAP))
{
Map model = (Map)paramObject;
/**
* 首先查找 # 包围的字符串,同时trim()去除前后的空白
* */
//"#[0-9a-zA-Z]*#"
Pattern p = Pattern.compile("#[0-9a-zA-Z]*#");
Matcher m = p.matcher(statement);
int index = 1;
while (m.find())
{
String key = m.group().replaceAll("#", "").trim();
System.out.println("key:" + key);
Object value = model.get(key);
pstmt.setObject(index++, value);
}
return pstmt;
}
/**
* 按属性名取出值作为输入参数 - 用户自定义的映射
* */
else if(sqlMapStatement.getParameterClass().getName().equals(TYPE_PARAMETER_MAP))
{
//占不支持自定义参数映射
return pstmt;
}
/**
* 按属性名取出值作为输入参数 - 用户自定义的类型
* */
//用户自定义的类型
else
{
Class<?> clazz = Class.forName(paramObject.getClass().getName());
/**
* 首先查找 # 包围的字符串,同时trim()去除前后的空白
* */
//"#[0-9a-zA-Z]*#"
Pattern p = Pattern.compile("#[0-9a-zA-Z]*#");
Matcher m = p.matcher(statement);
int index = 1;
while (m.find())
{
String key = m.group().replaceAll("#", "").trim();
Method method = clazz.getDeclaredMethod(getMethodName("get", key));
Object value = method.invoke(paramObject);
pstmt.setObject(index++, value);
}
return pstmt;
}
}
return pstmt;
6. 执行预编译SQL语句
返回执行的结果集。
关键代码:
ResultSet rs = pstmt.executeQuery();
7. 获取结果集
根据配置文件中定义的结果类型,获取查询结果集。
7.1 查看是否存在映射类型
解析配置文件,查找是否有 id 属性值与结果集类型相同的<resultMap>标签。若存在的话,将<resultMap>标签的子标签<result>对应的值设置到ResultMap实例中;否则,返回 null ,表示不存在对应的映射类型。
关键代码:
private ResultMap isResultMapExist(String className) throws Exception
{
ResultMap resultMap = null;
// System.out.println("className:" + className);
List<Element> resultMapElements = root.getChildren(NODE_RESULT_MAP);
if(resultMapElements.size() == 0)
{
throw new Exception("未配置" + NODE_RESULT_MAP + "节点");
}
/**
* 查找<resultMap>节点
* */
for(int i=0; i<resultMapElements.size(); i++)
{
Element resultMapElement = resultMapElements.get(i);
if(resultMapElement.getAttributeValue(ATT_ID).equals(className))
{
resultMap = new ResultMap();
resultMap.setClassName(resultMapElement.getAttributeValue(ATT_CLASS));
List<Element> resultElements = resultMapElement.getChildren(NODE_RESULT);
if(resultElements.size() == 0)
{
throw new Exception("未配置" + NODE_RESULT + "节点");
}
/**
* 查找<result>节点
* */
for(int j=0; j<resultElements.size(); j++)
{
Element result = resultElements.get(j);
resultMap.set(result.getAttributeValue(ATT_COLUMN), result.getAttributeValue(ATT_PROPERTY));
}
}
}
return resultMap;
}
7.2 根据映射存在与否创建 Class 实例
先查看结果集是否是自定义的映射类型,若是则创建 com.iteye.jarg.test.ResultMap 类型的 Class 类实例;否则创建结果集定义的结果类型的 Class 类实例。
7.3 设置结果集
从结果集中获取表字段信息,然后根据字段名,通过反射机制,调用对应的setter方法,将结果设置到对应的实例(结果集)中。
注:对于配置文件中定义的结果集类型是自定义的映射类型,需要先将表字段名转换成映射名。如:将表字段名 username 映射到com.iteye.jarg.test.User2 中字段 name。
注:对于查询结果集包含多条语句的情况,只需将下面代码中 if(rs !=null && rs.next()) 修改为 while(rs !=null && rs.next()) ,另外,用一个 java.util.List 实例保存多条结果。
关键代码:
if(rs != null && rs.next())
{
/**
* 根据映射存在与否创建 Class 实例
* 结果包装成输出类型
* */
Class<?> clazz = null;
ResultMap resultMap = isResultMapExist(sqlMapStatement.getResultClass());
if(resultMap == null)
clazz = Class.forName(sqlMapStatement.getResultClass());
else
clazz = Class.forName(resultMap.getClassName());
obj = clazz.newInstance(); //保存转换后的结果集
ResultSetMetaData metaData = rs.getMetaData(); //从结果集中获取表字段信息
for(int i=1; i<=metaData.getColumnCount(); i++)
{
String fieldName = metaData.getColumnName(i);
/**
* 先查看用户自定义映射,若不存在这一类型的映射类型则直接创建
* 通过表名能找到对象中函数名
* */
if(resultMap != null)
{
/**
* 将数据表字段转换成对应的映射类属性
* */
String fieldName2 = convert(sqlMapStatement.getResultClass(), fieldName);
Field field = obj.getClass().getDeclaredField(fieldName2);
Method method = obj.getClass().getDeclaredMethod(getMethodName(PREFIX_SET, fieldName2), field.getType());
method.invoke(obj, rs.getObject(fieldName));
}
else
{
Field field = clazz.getDeclaredField(fieldName);
Method method = clazz.getDeclaredMethod(getMethodName(PREFIX_SET, metaData.getColumnName(i)), field.getType());
method.invoke(obj, rs.getObject(fieldName));
}
}
}
8. 数据库查询用户接口
只简单做了数据库单条结果、多条结果查询功能,其他数据库操作类同。
通过反射机制,调用各结果集类型中的 display() 方法输出结果。
关键代码:
/**
* 查询,返回 Object 对象
* @param id String - SQL语句编号
* @param paramObject - 查询参数
* @throws SQLException
* */
public Object queryForObject(String id, Object paramObject) throws Exception
{
System.out.println(id);
Object obj = sqlMapConfig.queryForObject(id, paramObject);
if(obj == null)
{
System.out.println("**** 结果集为空 ****");
System.out.println("--------------------------");
return null;
}
Method method = obj.getClass().getMethod("display");
method.invoke(obj);
//((User)obj).display();
return obj;
}
/**
* 查询,返回 List 对象
* @param id String - SQL语句编号
* @param paramObject - 查询参数
* @throws Exception
* */
public List queryForList(String id, Object paramObject) throws Exception
{
System.out.println(id);
List list = sqlMapConfig.queryForList(id, paramObject);
if(list.size() == 0)
{
System.out.println("**** 结果集为空 ****");
System.out.println("--------------------------");
return null;
}
for(int i=0; i<list.size(); i++)
{
Method method = list.get(i).getClass().getMethod("display");
method.invoke(list.get(i));
//((User)list.get(i)).display();
}
return list;
}
9. 测试语句
测试单条结果、多条结果返回情况;复杂类型参数情况,自定义映射类型结果情况;多个输入参数情况。
获取数据库连接方法:[反射机制]建立数据库连接]
关键代码:
public static void main(String[] args) throws Exception
{
User user = new User();
user.setId(1);
user.setUsername("jarg");
user.setPassword("jarg");
User2 user2 = new User2();
user2.setId(2);
user2.setName("yeshaoting");
user2.setPwd("yeshaoting");
Map<String, String> model = new HashMap<String, String>();
model.put("id", "3");
model.put("username", "admin");
model.put("password", "admin");
List<Object> list = new ArrayList<Object>();
list.add("admin");
list.add("admin");
SqlMapBuilder builder = SqlMapBuilder.build("com/iteye/jarg/resources/mapper-config.xml");
Object obj = null;
//query
obj = builder.queryForObject("query", user2);
//queryUserById2
obj = builder.queryForObject("queryUserById2", model);
//queryUserById, queryUserByUsername, queryUserWithMap, queryUserWithList
obj = builder.queryForObject("queryUserById", 1);
obj = builder.queryForObject("queryUserByUsername", "yeshaoting");
obj = builder.queryForObject("queryUserWithMap", model);
obj = builder.queryForObject("queryUserWithList", list);
obj = builder.queryForObject("queryUser", null);
//queryUsersById, queryUsersByUsername, queryUsersWithMap, queryUsersWithList, queryAllUser
obj = builder.queryForList("queryUsersById", 1);
obj = builder.queryForList("queryUsersByUsername", "yeshaoting");
obj = builder.queryForList("queryUsersWithMap", model);
obj = builder.queryForList("queryUsersWithList", list);
obj = builder.queryForList("queryAllUser", null);
}
输出结果:
query
id: 2
name: yeshaoting
pwd: yeshaoting
--------------------------
queryUserById2
key:id
id: 3
name: admin
pwd: admin
--------------------------
queryUserById
id: 1
username: jarg
password: jarg
--------------------------
queryUserByUsername
id: 2
username: yeshaoting
password: yeshaoting
--------------------------
queryUserWithMap
key:username
key:password
id: 3
name: admin
pwd: admin
--------------------------
queryUserWithList
id: 3
username: admin
password: admin
--------------------------
queryUser
id: 2
username: yeshaoting
password: yeshaoting
--------------------------
queryUsersById
id: 2
username: yeshaoting
password: yeshaoting
--------------------------
id: 3
username: admin
password: admin
--------------------------
queryUsersByUsername
id: 1
username: jarg
password: jarg
--------------------------
id: 3
username: admin
password: admin
--------------------------
queryUsersWithMap
key:username
key:password
id: 1
name: jarg
pwd: jarg
--------------------------
id: 2
name: yeshaoting
pwd: yeshaoting
--------------------------
queryUsersWithList
id: 1
username: jarg
password: jarg
--------------------------
id: 2
username: yeshaoting
password: yeshaoting
--------------------------
queryAllUser
id: 1
username: jarg
password: jarg
--------------------------
id: 2
username: yeshaoting
password: yeshaoting
--------------------------
id: 3
username: admin
password: admin
--------------------------