试想一下,如果没有ibatis,程序员是如何处理数据库操作,并将结果转换为javabean的。以查询为例子,首先我们看下如何查询数据:
public class Main {
private static Map<String, String> nameMap = new HashMap<String, String>();
static{
nameMap.put("ID", "id");
nameMap.put("CREATED_AT", "createdAt");
nameMap.put("UPDATED_AT", "updatedAt");
}
static class Test{
private Long id;
private String createdAt;
private String updatedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "id:"+id+",createdAt:"+createdAt+",updatedAt:"+updatedAt;
}
}
public static void main(String[] args){
Connection connection = null;
PreparedStatement ps = null;
ResultSet resultSet = null;
List<Map<String, String>> resultMap = new ArrayList<Map<String,String>>();
try {
// 加载JDBC驱动
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
String url = "xxxxx";
String user = "xxxx";
String password = "xxxx";
connection = DriverManager.getConnection(url, user, password);
String sqlString = "select * tests where test_id=?";
ps = connection.prepareStatement(sqlString);
ps.setLong(1, 1667505);
resultSet = ps.executeQuery();
ResultSetMetaData rsData = resultSet.getMetaData();
int columnCount = rsData.getColumnCount();
while(resultSet.next()){
Map<String, String> map = new HashMap<String,String>();
for(int i=0;i<columnCount;i++){
String columnString = rsData.getColumnName(i+1);
map.put(columnString, resultSet.getString(i+1));
}
resultMap.add(map);
}
List<Test>list = convert(resultMap);
System.out.println(list);
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(null != resultSet){
resultSet.close();
}
if(null != ps){
ps.close();
}
if(null != connection){
connection.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
private static List<Test> convert(List<Map<String, String>> resultMap) throws Exception {
List<Test> list = new ArrayList<Test>();
for(Map<String,String> map : resultMap){
Test test = new Test();
for(String key : map.keySet()){
String fieldName=nameMap.get(key);
if(StringUtils.isNotBlank(fieldName)){
Class class1 = Test.class.getDeclaredField(fieldName).getType();
Object val=converToRealObject(map.get(key),class1);
String methodName = "set"+fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);
Method method = Test.class.getDeclaredMethod(methodName, class1);
method.invoke(test, val);
}
}
list.add(test);
}
return list;
}
private static Object converToRealObject(String value, Class class1) throws Exception {
if(String.class == class1){
return value;
}
String className=class1.getName();
Object object=null;
switch (className) {
case "java.lang.Long":
case "java.lang.Integer":
case "java.lang.Float":
case "java.lang.Double":
case "java.math.BigDecimal":
Method method = class1.getDeclaredMethod("valueOf", String.class);
object = method.invoke(class1, value);
break;
default:
break;
}
return object;
}
}
针对上面的代码,我们来思考下几个问题:
1,连接的获取与释放
上面的代码是在执行一个数据库操作时编写的代码,试想,如果有多个数据库操作,那数据库连接频繁的创建与释放会造成极大的资源消耗,影响系统性能.这个问题可以通过使用数据库连接池来解决.
2,sql语句写在了java代码中,编写的时候还需要+号进行连接,不利于维护和阅读,如果修改了sql语句,还得通过编译java代码才能生效。可以考虑把有关的sql语句放入到一个配置文件中,以key-value形式存储
3,如果以key-value格式存储针对静态的sql没有问题,但是如果是动态sql呢,传入的参数是可变的,这时候就需要动态拼成完整sql了。可以考虑在sql语句中引入变量,变量名跟一个map关联,过map来获取变量的值,那这时就需要一个sql解析器了,需要解析出配置文件中的sql,定位哪些是变量,用于在真实查询的时候替换成合适的值。
4,查询出来的结果是resultset,需要转换成javabean,首先需要知道该转换成哪个bean,以及字段间的对应关系,其次如果返回是list,需要构造list型的返回,上面的代码在转换时的做法不够优雅,不大可取,得考虑一种可以扩展的方式进行类型转换.
5,上面代码中的共性可以抽取出来,简化代码编写
围绕着以上几个问题,下面我们自己动手来实现一个简易的ibatis框架,我们还是根据从上到下的方式,按照需求,结合之前的文章,一步步完成我们的框架。
首先编写测试程序:
public class Main {
public static void main(String[] args) throws Exception {
SqlMapClient sqlMapClient = new SqlMapClientImpl("sqlmapConfig.xml");
long userId = 1l;
UserDTO user = (UserDTO) sqlMapClient.selectForObject("user.findById",userId);
}
}
接下来定义接口sqlmapclient,为什么要定义接口?一般来说,我们对数据库的操作不外乎增删改查,这些行为具有一定的共性,抽取出来,定义为接口,以后如果有其他方式提供查询服务,也需要实现这个接口:
public interface SqlMapClient {//暴露给客户端
/**
* insert
* @param sql
* @param object
* @return
*/
public Object insert(String sql,Object object)throws SQLException;
/**
* select one
* @param sql
* @param parameterObject
* @return
*/
public Object selectForObject(String sql,Object parameterObject)throws SQLExceptio;
/**
* select list
* @param sql
* @param parameterObject
* @return
*/
public List<Object> selectForList(String sql, Object parameterObject)throws SQLExceptio;
/**
* update
* @param sql
* @param parameterObject
* @return
*/
public int update(String sql,Object parameterObject)throws SQLException;
}
此处我们暂时先考虑实现select one.继续看下sqlmapConfig.xml中的配置:
<?xml version="1.0" encoding="UTF-8"?>
<sqlmap resource="user/user.xml"/>
这个配置文件后续可以添加更多的sqlmap,再看下user,xml:
<?xml version="1.0" encoding="UTF-8" ?>
<sqlMap namespace="user" >
<resultMap id="userMap" class="com.my.ibatis.UserDTO" >
<result column="id" property="id" jdbcType="DECIMAL" />
<result column="user_name" property="userName" jdbcType="VARCHAR" />
<result column="age" property="age" jdbcType="DECIMAL" />
<result column="sex" property="sex" jdbcType="VARCHAR" />
</resultMap>
<select id="findById" resultMap="userMap" parameterClass="long" >
select * from user where id=#id#
</select>
</sqlMap>
在这个xml中,我们定义了从数据库中查询出来的数据与javabean之间映射关系,以及sql语句信息,上面的查询语句中,#id#,id就表示我们思考的问题中的一个变量,其类型为long,sql查询的结果时一个userMap。按照上面的xml,我们定义一个类SqlMapConfig类表示xml中的数据信息:
public class SqlMapConfig {
private Map<String,SqlMapInfo> mappedSql = new HashMap<String,SqlMapInfo>();//根据sql id能够快速找到sql
static class Sql{
private String sqlString;//sql语句
private String sqlId;
private String resultMapId;//
private ResultMap resultMap;//结果集resultMap
private Map<String,Integer> parameterIndexMap;//参数index映射
private Class parameterClass;
}
static class SqlMapInfo {
private String resource;//sql id属于哪个resource
private Sql sql;
}
static class ResultMap{
private String id;
private String classz;
private Map<String, String> propertyMap;//字段名映射
}
static class Result{
private String column;
private String property;
private String jdbcType;
}
}
接下来,就应该考虑实现我们的查询功能了.先考虑几个问题:
1,sql查询肯定需要先建立连接的,连接如何管理?
2,一个sqlMapClient可能有多个连接在使用,如何保证各个线程之间不受彼此的影响?
为解决1的问题,可以引入连接池,这个有现成的框架可以用,这里我们先暂不考虑。主要看下2的问题,简单来说就是要起到线程隔离的作用,彼此互不干扰,很自然的我们想到了使用ThreadLocal.我们将上面的代码整改下,使得接口行为更加清晰点,定义个接口SqlMapExecutor,其主要用于数据库的操作行为,SqlMapClient继承它,同时在SqlMapClient中我们引入一个SqlMapSession接口,用于隔离各个线程,其行为主要有打开和关闭:
public interface SqlMapClient extends SqlMapExecutor{//暴露给客户端
/**
* 开启一个会话
* @return
*/
public SqlMapSession open();
}
相当于原来sqlmapclient的功能委托为这个sqlMapSession来实现了,所以sqlMapSession需要继承SqlMapExecutor,既然是会话,肯定有关闭的行为,所以定义了close方法:
public interface SqlMapSession extends SqlMapExecutor{
public void close();
}
这里有个问题,session该什么时候关闭?对于传统JDBC Connection 而言,我们获取Connection 实例之后,需要调用Connection.setAutoCommit 设定事务提交模式。在AutoCommit 为true 的情况下,JDBC 会对我们的操作进行自动提交(相当于不带事务操作),此时,每个JDBC 操作都是一个独立的任务,自动提交后,session就可以关闭了。.那session关闭时需要执行哪些操作?清除resultset,statment,关闭连接都是需要执行的操作。好了,有了以上的问题,我们可以下设计下相关类了。
首先,我们看下在“不带事务操作”的情况下,如何进行sql操作,还是以查询为例子:
public Object selectForObject(String sqlId, Object parameterObject) throws SQLException {
SqlMapConfig.Sql sql = this.sqlMapClient.getSqlMapConfig().getMappedSql().get(sqlId).getSql();
PreparedStatement ps = null;
ResultSet resultSet = null;
try {
ps = connection.prepareStatement(sql.getSqlString());
//set paramters
if(sql.getParameterClass() == parameterObject.getClass()){
TypeHandlerFactory.getTypeHandler(sql.getParameterClass()).setParamters(ps,sql.getParameterIndexMap(),parameterObject);//设置statement参数
resultSet = ps.executeQuery();
return buildResultObject(resultSet,sql);//构造返回对象
}else{
throw new SQLException("wrong parameter type");
}
}catch (Exception e){
throw new SQLException(e);
}finally {
cleanup(ps,resultSet);
}
}
我们知道PreparedStatement设置参数时是根据类型设置的,比如setInt,setString方法等,为此我们添加了TypeHandler接口,其行为主要有:
public interface TypeHandler {
public void setParamters(PreparedStatement preparedStatement, Map<String, Integer> parameterMap, Object value) throws SQLException;
public Class getParameterClass(String name)throws SQLException;
public void setValueForObject(ResultSet resultSet, Object object, String columnName, String propertyName)throws SQLException;
}
第一个方法主要用于设置参数,第二个方法用于根据xml元素parameterClass得到对应的Class对象,第三个方法用于将resultset中元素填充到javabean对象中。比如IntegerTypeHandler,其实现如下:
public class IntegerTypeHandler implements TypeHandler {
@Override
public void setParamters(PreparedStatement preparedStatement, Map<String,Integer>parameterMap, Object value) throws SQLException {
//只有一个
Integer index = (Integer) (parameterMap.values().toArray()[0]);
preparedStatement.setInt(index+1,(Integer)value);
}
@Override
public Class getParameterClass(String name) {
return Integer.class;
}
@Override
public void setValueForObject(ResultSet resultSet, Object object,
String columnName, String propertyName) {
try {
int v = resultSet.getInt(columnName);
String methodName="set"+propertyName.substring(0,1).toUpperCase()+propertyName.substring(1);
Method method = object.getClass().getDeclaredMethod(methodName, int.class);
method.invoke(object, v);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
至于具体使用哪个TypeHandler,将由TypeHandlerFactory提供。cleanup方法中,主要是关闭resultSet,preparedStatemet,session:
private void cleanup(PreparedStatement ps, ResultSet resultSet) {
try{
if(null != resultSet && !resultSet.isClosed()){
resultSet.close();
}
if(null != ps && !ps.isClosed()){
ps.close();
}
this.close();
}catch (Exception e){
throw new RuntimeException(e);
}
}
以上都是在没有事务操作情况下的实现方式,下面我们考虑下载事务操作下,该如何实现,首先我们先编写一个测试程序:
//事务
try{
sqlMapClient.startTransaction();
sqlMapClient.insert("user.insert",user);
sqlMapClient.update("user.update",user);
sqlMapClient.commitTransaction();
}catch (Exception e){
e.printStackTrace();
}finally {
sqlMapClient.endTransaction();
}
其中的insert和update操作需要在一个事务中完成,根据测试程序的需要,我们一步步完成带事务的框架。首先我们定义一个接口,事务管理器,用于事务行为:
public interface SqlMapTransactionManager {
public void startTransaction() throws SQLException;
public void commitTransaction() throws SQLException;
public void endTransaction() throws SQLException;
}
SqlMapClient需要继承它,根据上面的分析,需要交由sqlMapSession的实现类实现事务的管理,为此,SqlMapSession类需要继承SqlMapTransactionManager接口,这三个方法的实现为:
public void startTransaction() throws SQLException {
transaction = new TransactionImpl();
transaction.getConnection().setAutoCommit(false);
}
@Override
public void commitTransaction() throws SQLException {
transaction.getConnection().commit();
}
@Override
public void endTransaction() throws SQLException {//事务结束时需要判断是否要回滚,然后再执行清除的操作
Connection connection = transaction.getConnection();
if(needRollback){
connection.rollback();
}
if(null != connection && !connection.isClosed()){
connection.close();
}
this.close();
}
下面以insert为例,看下在有事务的情况下如何实现:
public Object insert(String sqlId, Object object) throws SQLException {
Transaction transaction = getTransaction();
boolean autoCommit = (null == transaction ||transaction.getConnection().getAutoCommit());
try{
transaction = getCurrentTransaction(transaction,autoCommit);
Object object1 = sqlExecutor.executeInsert(sqlId,transaction,object);
autoCommitTransaction(transaction,autoCommit);
return object1;
}catch (Exception e){
if(!autoCommit){
needRollback = true;
}
throw new SQLException(e);
}finally {
autoEndTransaction(transaction,autoCommit);
}
}
private Transaction getCurrentTransaction(Transaction transaction, boolean autoCommit) {
Transaction t = transaction;
if(autoCommit){
t = new TransactionImpl();
}
return t;
}
private void autoCommitTransaction(Transaction transaction, boolean autoCommit) throws Exception {
}
private void autoEndTransaction(Transaction transaction, boolean autoCommit) {
if(autoCommit){
try{
if(!transaction.getConnection().isClosed()){
transaction.getConnection().close();
}
}catch (Exception e){
throw new RuntimeException(e);
}
this.close();
}
}
这里考虑了自动提交与非自动提交的兼容,当getTransaction不为空时,说明此时开启了事务,否则还是当做自动提交处理。按照上面的实现方式,如果与spring结合时,只需要在获取connection时将其设置为非自动提交即可!
以上代码均已上传:https://github.com/reverence/myibatis
这里结合前面几篇文章,谈谈对框架的理解。个人认为,框架的作用在于将流程中不变的东西(共性)用代码固化下来,将变化的东西通过配置文件或者使用一定的策略模式方便的进行扩展,以达到提高程序开发效率的目的!