写在前面的

这一章是之前写的《手把手教你写一个Java的orm框架》 的追加内容。因为之前写的数据库框架不支持级联查询这个操作,对于有关联关系的表用起来还是比较麻烦,于是就准备把这个功能给加上。这个功能是在我之前写的数据库框架基础上做的,有兴趣的同学可以看一看。

数据库框架

github:JdbcPlus

关于这个框架

手把手教你写个java的orm框架(1)

手把手教你写个java的orm框架(2)

手把手教你写个java的orm框架(3)

手把手教你写个java的orm框架(4)

手把手教你写个java的orm框架(5)

大致的思路

对于级联查询这个操作,他用起来大致是这样的,比如:

//在这里使用数据库框架查询一个对象出来
User user = jdbcPuls.selectById(User.class, 1);
//user对象中有一个parent属性,关联的是另外一个表,在数据库中是一个外键
//这时候我们直接使用getParent(),就可以将关联对象查出来
Parent parent = user.getParent();

要实现这个功能,首要条件是需要在第一次查询的时候返回一个代理对象,这个代理对象会拦截到所有的get方法,并且需要在方法中判断:如果这个方法对应的属性是一个外键的话,就通过数据库将这个对象查出来(还是个代理对象),然后返回出去。

需要用到的技能

根据上面的思路来说,主要使用到的就是动态代理。动态代理有很多种实现方式,比如java的动态代理cglib的动态代理等等。这里我使用cglib的动态代理,原因吗...是因为java的动态代理必须要基于一个接口,我又不想修改数据库对应的实体类,那就只能用cglib咯。 关于cglib呢,它是一个Java的字节码框架,官方描述是这样的:

 Byte Code Generation Library is high level API to generate and transform JAVA byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access. https://github.com/cglib/cglib/wiki

是不是很厉害的样子。cglib的动态代理是基于子类实现的(它是直接生成了一个子类的class文件,通过一定的配置可以得到生成的class文件),然后在子类中调用父类的方法这样的。具体原理这里就不多说了,我们只需要知道怎么用就好了。

用cglb创建代理对象

使用cglib创建代理对象的方法大致是这样的:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
//设置方法回调
enhancer.setCallback(new MethodInterceptor() {
/**
* 被代理的对象方法执行前,会调用这个方法,用于方法的拦截
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(
Object o,
Method method,
Object[] objects,
MethodProxy methodProxy
) throws Throwable {
//原方法的执行结果
Object invokeResult = methodProxy.invokeSuper(o, objects);
//这里可以写一些别的东西
return invokeResult;
}
});
//创建代理对象
User proxyUser = (User) enhancer.create();

这样创建代理对象的这一部分就完成啦,接下来就是要想一下怎样实现级联查询这个功能了。

实现级联查询

写一个注解描述关联关系

之前我写的数据库框架:JdbcPlus 是通过注解来实现的,我要新增一个注解用来描述一个实体类的属性另一个实体类之间的关联关系。这里我自定义一个注解 @FK,代码如下

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* 添加在外键字属性上,
* 属性类型是关联对象的Entity
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FK { /**
* 被关联的字段名称
*
* @return
*/
String value(); }

注解中的value表示被关联对象的字段名称。下面创建一个实体类来说明一下:

/**
* 用户表
*
*/
@Getter
@Setter
@Table("user")
public class User { /**
* 用户名
*/
@Column("name")
private String name; /**
* 用户id
*/
@Id
@Column("id")
private int id; /**
* parent_id
*/
@Column("parent_id")
@FK("id")
private User parentId; }

1:@Table("user")说明User对象对应数据库中的user表。 2:@Column("parent_id") 表示这个属性对应的是sql中的字段parent_id3:@FK("id")说明这个字段是一个外键,关联到user表中的字段id上。 4:@Getter和@Setter是lombok中的注解,用于生成对应的get/set方法,不重要。

修改框架查询方法,返回代理对象

这里要将之前写的数据库框架返回的查询结果修改为代理对象:

/**
* 把数据库查询的结果与对象进行转换
*
* @param resultSet
* @param rowNum
* @return
* @throws SQLException
*/
@Override
@SneakyThrows(SQLException.class)
public T mapRow(ResultSet resultSet, int rowNum) {
Map<String, Object> resultMap = columnMapRowMapper.mapRow(resultSet, rowNum);
//创建cglib代理对象
EntityProxy entityProxy = EntityProxy.entityProxy(tableClass, jdbcPlus);
Object proxy = entityProxy.getProxy();
for (Map.Entry<String, Object> entry : resultMap.entrySet()) {
//数据库字段名
String key = entry.getKey();
if (!columnFieldMapper.containsKey(key)) {
continue;
}
Field declaredField = columnFieldMapper.get(key);
if (declaredField == null) {
continue;
}
Object value = entry.getValue();
//如果属性添加了@FK注解,新建一个空对象占位
if (EntityUtils.isFK(declaredField)) {
Object fkObject = getJoinFieldObject(declaredField, value);
ClassUtils.setValue(proxy, declaredField, fkObject);
} else {
ClassUtils.setValue(proxy, declaredField, value);
}
}
return (T) proxy;
} /**
* 用于填充查询对象,使其toString中外键值不显示null
*
* @param fkField 外键属性
* @param sqlValue sql中的结果
* @return
*/
Object getJoinFieldObject(Field fkField, Object sqlValue) {
if (sqlValue == null) {
return null;
}
Class fieldType = fkField.getType();
//找到对应的Class
EntityTableRowMapper mapper = EntityMapperFactory.getMapper(fieldType);
Map<String, Field> mapperColumnFieldMapper = mapper.getColumnFieldMapper();
FK FK = EntityUtils.getAnnotation(fkField, FK.class);
String fieldName = FK.value();
//实例化原始对象,与之后的代理对象做区分
Object entityValue = ClassUtils.getInstance(fieldType);
Field field = mapperColumnFieldMapper.get(fieldName);
ClassUtils.setValue(entityValue, field, sqlValue);
return entityValue;
}

这里是将数据库查询结果转换成一个实体类的方法,是SpringJDBC中接口RowMapper的一个实现类。这主要修改的部分是:

1:在遍历属性并赋值的部分添加了属性上有没有注解@FK的判断。

2:如果属性上添加了@FK,就根据属性的类型,实例化一个原始对象,并把查询结果的值放进这个原始对象的对应属性中。

3:将查询的返回结果修改成代理对象。这里创建代理对象的类是EntityProxy,这个在后面会说明。

方法拦截器

这里主要就是在说EntityProxy这个类,它主要做的事情就是创建代理对象,并且拦截对象中的方法。代码是这样的:

package com.hebaibai.jdbcplus;

import com.hebaibai.jdbcplus.util.ClassUtils;
import com.hebaibai.jdbcplus.util.EntityUtils;
import com.hebaibai.jdbcplus.util.StringUtils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.util.Assert; import java.lang.reflect.Field;
import java.lang.reflect.Method; /**
* entity的代理对象
*
* @author hjx
*/
@CommonsLog
public class EntityProxy implements MethodInterceptor { /**
* 代理对象
*/
@Getter
private Object proxy; /**
* 数据库操作工具
*/
private JdbcPlus jdbcPlus; /**
* 创建代理对象
*
* @param entityClass
* @return
*/
public static EntityProxy entityProxy(Class entityClass, JdbcPlus jdbcPlus) {
log.debug("创建代理对象:" + entityClass.getName());
EntityProxy entityProxy = new EntityProxy();
Assert.isTrue(EntityUtils.isTable(entityClass), "代理对象不是一个@Table!");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(entityClass);
//设置方法回调
enhancer.setCallback(entityProxy);
//创建代理对象
entityProxy.proxy = enhancer.create();
entityProxy.jdbcPlus = jdbcPlus;
return entityProxy;
} /**
* 代理对象方法拦截器,用于实现几联查询
*
* @param entity
* @param method
* @param values
* @param methodProxy
* @return
*/
@Override
@SneakyThrows(Throwable.class)
public Object intercept(Object entity, Method method, Object[] values, MethodProxy methodProxy) {
//执行原本的方法
Object invokeResult = methodProxy.invokeSuper(proxy, values);
Class fkEntityClass = method.getReturnType();
String name = method.getName();
//返回值位null,直接返回
if (invokeResult == null) {
return invokeResult;
}
//如果是get方法, 或者 boolean 类型的is 开头
else if (name.startsWith("get") || name.startsWith("is")) {
Class invokeResultClass = invokeResult.getClass();
Class superclass = invokeResultClass.getSuperclass();
//如果父类等于字段类型并且添加了@Table注解,
// 说明是cglib生成的子类并且已经查询出来了结果,直接返回
if (fkEntityClass == superclass || !EntityUtils.isTable(fkEntityClass)) {
return invokeResult;
}
//通过方法名找到Entity的属性,之后找到该属性关联的Entity中的属性。
Field fkField = getFieldBy(method);
Field fkTargetField = EntityUtils.getEntityFkTargetField(fkField);
if (fkTargetField == null) {
return invokeResult;
}
Column column = EntityUtils.getAnnotation(fkTargetField, Column.class);
Object value = ClassUtils.getValue(invokeResult, fkTargetField);
//执行查询
log.debug("对外键属性进行数据库查询。。。");
Object fkEntityProxy = jdbcPlus.selectOneBy(fkEntityClass, column.value(), value);
//将查询结果赋值给原对象
ClassUtils.setValue(this.proxy, fkField, fkEntityProxy);
return fkEntityProxy;
} else {
return invokeResult;
}
} /**
* 通过方法找到对应的属性
*
* @param method
* @return
*/
private Field getFieldBy(Method method) {
String fieldName = method.getName();
if (fieldName.startsWith("get")) {
fieldName = fieldName.substring(3);
fieldName = StringUtils.lowCase(fieldName, 0);
} else if (fieldName.startsWith("is")) {
fieldName = fieldName.substring(2);
fieldName = StringUtils.lowCase(fieldName, 0);
} else {
//没有以get 或者 is 开头的方法,直接返回null
return null;
}
//通过属性名找到class中对应的属性
Class<?> declaringClass = method.getDeclaringClass();
try {
Field field = declaringClass.getDeclaredField(fieldName);
//如果找到的属性字段类型与方法返回值不同,返回null
if (field.getType() != method.getReturnType()) {
return null;
}
return field;
} catch (NoSuchFieldException e) {
return null;
}
} /**
* 私有化构造器
*/
private EntityProxy() {
}
}

说一下其中的几个属性和方法。

1:private Object proxy; 创建出来的代理对象。

2:private JdbcPlus jdbcPlus; 操作数据库的类,就是这个框架本身主要的类。

3:public static EntityProxy entityProxy(Class entityClass, JdbcPlus jdbcPlus); 创建代理对象的方法,创建一个entityClass的代理对象。

4:public Object intercept(Object entity, Method method, Object[] values, MethodProxy methodProxy); 实现了cglib的接口MethodInterceptor后需要重写的方法,也是实现级联查询功能主要要写的方法。这里传入了五个参数:

entity:对象本身。

method:代理对象所拦截的方法本身。

values:方法中传入的参数。

methodProxy:拦截的方法的代理。

在进入这个方法的时候先做了一些校验,比如方法是不是一个get方法,返回值的类型是不是添加了@Table注解,返回值是不是null等等,在这些条件满足的情况下,取出这个方法的返回值invokeResultClass(此时这个对象是一个原始对象,是在上面的getJoinFieldObject中创建出来的)中保存的sql中查询出来的值,最后通过方法名找到对应的添加了@FK注解的属性找到所关联的表,执行查询并得到查询结果(这时结果是一个代理对象),最后吧查询结果设置到对应的属性上,并返回就好了。

最后

这里部分写的比较混乱(吃了没文化的亏/(ㄒoㄒ)/~~),但是代码上还是比较清楚的,可以结合代码看一下。主要的部分就是EntityProxy.interceptEntityTableRowMapper.mapRow两个方法,大家可以上github看一下。

阅读原文

使用cglib实现数据库框架的级联查询的更多相关文章

  1. 【SSH网上商城项目实战05】完成数据库的级联查询和分页

    转自:https://blog.csdn.net/eson_15/article/details/51320212 上一节我们完成了EasyUI菜单的实现.这一节我们主要来写一下CategorySer ...

  2. ORACLE 数据库的级联查询 一句sql搞定(部门多级)

    在ORACLE 数据库中有一种方法可以实现级联查询   select  *                //要查询的字段 from table              //具有子接点ID与父接点I ...

  3. tp框架where条件查询数据库

    tp框架where条件查询数据库 Where 条件表达式格式为: $map['字段名'] = array('表达式', '操作条件'); 其中 $map 是一个普通的数组变量,可以根据自己需求而命名. ...

  4. iOS---SQLite数据库框架之FMDB -Swift

    SQLite数据库框架之FMDB 什么是FMDB? FMDB是iOS平台的SQLite数据库框架,FMDB以OC的方式封装了SQLite的C语言API.对比苹果自带的Core Data框架,更加轻量级 ...

  5. Oracle级联查询

    在ORACLE 数据库中有一种方法可以实现级联查询   select  *                //要查询的字段 from table              //具有子接点ID与父接点I ...

  6. 一个响应式数据库框架SQLBrite,完美解决数据库和UI的同步更新!

    相信小伙伴们在开发中或多或少都可能遇到过这样的问题:打开一个应用后,为了快速响应,先将数据库中的数据呈现给用户,然后再去网络上请求数据,请求成功之后将数据缓存至数据库,同时更新UI,但是我们经常会这样 ...

  7. 玩转Android之数据库框架greenDAO3.0使用指南

    用过ActiveAndroid.玩过ORMLite,穿过千山万水,最终还是发现greenDAO好用,ActiveAndroid我之前有一篇文章介绍过 玩转Android之数据库框架ActiveAndr ...

  8. 玩转Android之数据库框架ActiveAndroid的使用

    ActiveAndroid是一个开源的数据库框架,使我们在Android中使用数据库变得更为简单,今天我们就来看看这个数据库框架的使用. 1.引入ActiveAndroid 首先创建我们自己的项目,在 ...

  9. Mybatis 之级联查询 一对多配置

    Mybatis级联 查询相对于hibenate是有点麻烦,但是相应好处也是有的,Mybatis轻量.根据自己要的字段配置方便 一对多配置用   <collection property=&quo ...

随机推荐

  1. 发送短信功能(C#)

    参考自:https://blog.csdn.net/whl632359961/article/details/73468115 https://www.cnblogs.com/ywl925/archi ...

  2. Decimal类型截取保留N位小数向上取, Decimal类型截取保留N位小数并且不进行四舍五入操作

    Decimal类型截取保留N位小数向上取Decimal类型截取保留N位小数并且不进行四舍五入操作 封装静态方法 public class DecimalHelper { /// <summary ...

  3. .net图表之ECharts随笔06-这才是最简单的

    今天搞柱形图的时候,发现了一个更简单的用法.那就是直接使用带all的那个js文件 基本步骤: 1.为ECharts准备一个具备大小(宽高)的Dom 2.ECharts的js文件引入(echarts-a ...

  4. MVC+Nhibernate+spring.net(一)

    所用数据库是我之前所写的Nhibernate入门篇的数据库https://www.cnblogs.com/pandorabox/p/PandoraBox.html 第一步:创建一个mvc项目 第二步: ...

  5. TextBox Ctrl+A不能全选的问题

    问题: 当TextBox控件在设置了MultiLine=True之后,Ctrl+A 无法全选,十分影响使用体验. 对于这个问题不明所以,不知道是Bug,还是故意而为之... 解决1: 添加KeyDow ...

  6. 一个简单文本分类任务-EM算法-R语言

    一.问题介绍 概率分布模型中,有时只含有可观测变量,如单硬币投掷模型,对于每个测试样例,硬币最终是正面还是反面是可以观测的.而有时还含有不可观测变量,如三硬币投掷模型.问题这样描述,首先投掷硬币A,如 ...

  7. Java知识锦囊

    最近突然想回顾一下之前的文章,回顾复习一下,正好把觉得还不错的文章收录到这里,方便查阅 Java 2018-04-02 Java计数器之CountDownLatch.CyclicBarrier.Sem ...

  8. cStringIO 实现指定大小的字符串缓存

    StringIO经常被用来作为字符串的缓存,以下实现无论写入多少字符串,总能返回一个指定大小的缓存 from cStringIO import StringIO class CustomStringI ...

  9. Java实现二分法排序

    二分法:(二分法不是只能做数组,这里的数组只是为了举例) 在给出的有序排列的数组中,把目标值和数组中间值进行比较,如果相等,则返回中间值下标,如果目标值小于中间值,就从数组的前半段再次执行二分法查找, ...

  10. C++ STL基本容器的使用(vector、list、deque、map、stack、queue)

    1.关联容器和顺序容器 C++中有两种类型的容器:顺序容器和关联容器,顺序容器主要有:vector.list.deque等.关联容器主要有map和set.如下图: 1.vector基本使用 #incl ...