配置关联关系

linpeilie大约 9 分钟指南

基础配置

当类型中,有需要关联查询获取的字段时,可以通过 @Relation 注解配置关联关系。 例如:


@Data
public class User {

    private String username;

    private String nickName;

    @Relation(provider = "getRoleByUsername", condition = {@Condition(field = "username")})
    private Role role;

}

上面的例子中,表示 User 中的 role 属性,需要通过唯一标识为 getRoleByUsername 的数据提供者(被 @DataProvider 注解标注的方法)来获取,且使用当前对象中的 username 属性作为关联条件。

关联条件配置

@Relation 注解中,可以通过 condition 注解,来配置关联条件,该属性支持配置多个关联条件。

在配置关联条件时,必须要在 @Condition#field 中指定使用当前类型中的哪个属性来关联。

@Condition 注解中,还有一个参数:paramField,该参数表示在关联查询时,当前对象中的关联属性,与关联查询对象中属性的对应关系。 即,在关联查询时,当前属性的值,需要赋值给入参对象中的哪个字段。 如果没有配置该属性的话,默认取 field 配置的名称。

例如:

基于快速开始中的示例,用户信息查询时需要 UserQueryReq 类型的参数, 当 Order 对象关联用户昵称时,会将当前订单中的 username 赋值给 UserQueryReq 中的 username 来进行查询。

多关联条件

上一节讲过可以通过配置多个 @Condition 来指定多个关联条件来查询,这里只做一个演示。

基于快速开始中的示例,OrderUserQueryReq 类中增加 source 字段,且关联查询用户昵称时,需要同时使用该字段,代码如下:

@Data
public class Order {

    private String source;

    private String orderId;

    private String username;

    @Relation(provider = RelationIdentifiers.getUserByUsername, targetField = "nickName",
        condition = {
            @Condition(field = "username"),
            @Condition(field = "source")
        })
    private String nickName;

}

执行 QuickStart 时,控制台会打印如下信息:

req = UserQueryReq(username=admin, userId=null, isDeleted=null, source=ODD)
Order(source=ODD, orderId=eb35e18caa552284b39d427c1e06f9f7, username=admin, nickName=管理员)

常量条件

有的时候,关联查询需要带一些固定的条件,例如:用户关联查询权限时,只能查询启用状态下的权限列表。

当这种情况时,可以通过 @Realtion#constantsCondion 来配置这种固定的条件。

例如:

@Data
public class User {

    private String username;

    @Relation(provider = RelationIdentifiers.getPermissionsByUsername, condition = {
        @Condition(field = "username")}, constantsCondition = {@ConstantsCondition(field = "state", value = "1")})
    private List<Permission> permissions;
    
}

控制台打印如下:

permissionQueryReq:PermissionQueryReq(username=admin, state=1)
User(username=admin, nickName=管理员, icon=null, role=Role(roleId=100001, roleName=系统管理员), permissions=[Permission(resource=/user), Permission(resource=/role), Permission(resource=/permission)], createUsername=admin, createNickName=管理员)

兼容单个参数

有的时候,在关联查询时,只有一个参数即可查出,不想因此建一个查询入参对象。框架针对这种情况做了特殊处理。

当满足以下条件:

  1. 数据提供者方法入参只有一个,且类型为 StringIntegerLong 任意一种;
  2. 类型中配置的关联关系只有一个关联条件。

例如:用户(User)模型中需要关联角色(Role),这里只想根据 username 来查询相应的角色信息。

@Data
public class User {

    private String username;

    @Relation(provider = "getRoleByUsername", condition = {@Condition(field = "username")})
    private Role role;

}

关联目标类型属性/平铺关联

有的时候,需要关联另一个模型中的部分属性,并不需要关联所有属性,但复用的关联数据查询接口,返回的是整个对象模型。 针对这种场景,在 @Relation 中提供了一个配置 —— targetField,该属性可以配置,获取到关联对象的具体属性值。

例如快速开始中,订单模型中需要根据 username 关联查询 nickName, 但提供的数据提供者返回的是 User 类,所以这里需要指定 targetFieldUser#nickName

这里需要注意,当需要从一个对象模型中关联多个对象时,并不会查询多次。例如,需要关联用户昵称和用户头像,只能查询一次。 详情可以查看缓存

启用缓存

针对于每一个类型中的每个关联属性,都可以单独配置是否启用缓存。

@Relation 注解中提供了属性:cacheStrategy 来自定义当前类关联该属性时的缓存策略。

其中提供了三个可选项:

  • ENABLE:开启缓存;
  • DEFAULT:默认配置,默认情况下,不自定义缓存策略,而是使用缓存提供者的缓存配置;
  • DISABLE:禁用缓存

这里需要注意三点:

  1. 注解中配置的缓存是指 EasyRelation 中的定义的二级缓存
  2. 当一个类中有平铺关联的场景时,针对于同一个数据提供者,缓存配置要相同!!!如果不同的话,不能够保证最终的缓存策略。
  3. 当缓存策略为非 DEFAULT 时,以当前属性上面配置的为准,与数据提供者配置的是否启用缓存无关。

具体可参考缓存

下面是一个使用缓存的例子:

定义一个文章类 Article,其中保存了作者用户名,需要关联作者昵称.

@Data
public class Article {

    private String content;

    private String authorUsername;

    @Relation(provider = RelationIdentifiers.getUserByUsername, condition = {
        @Condition(field = "authorUsername", paramField = "username")}, targetField = "nickName",
        cacheStrategy = CacheStrategy.ENABLE)
    private String nickName;

}

控制台输出如下:

2023-03-21 21:39:46req : UserQueryReq(username=admin, userId=null, isDeleted=null)
Article(content=EasyRelation是一个简单、快速、强大的数据自动关联框架, authorUsername=admin, nickName=管理员)
2023-03-21 21:39:51req : UserQueryReq(username=admin, userId=null, isDeleted=null)

转换异常

在关联时,如果出现异常,可以手动配置是否将该异常抛出。

@Relation 注解中提供了 exceptionStrategy 属性,用来配置关联数据发生异常时的处理策略,默认由 RelationProperties 指定(当前类中的默认值为抛出异常)。

基于前面的示例,在 UserInfoDataProvider 中增加判断,当传入 usernamenull 时,则抛出异常:

@DataProvider(RelationIdentifiers.getUserByUsername)
public User getUserByUsername(UserQueryReq req) {
    System.out.println(DateUtil.now() + "req : " + req);
    if (StrUtil.isEmpty(req.getUsername())) {
        throw new IllegalArgumentException("username is empty");
    }
    if ("admin".equals(req.getUsername())) {
        final User user = new User();
        user.setUsername("admin");
        user.setNickName("管理员");
        return user;
    }
    return null;
}

添加测试用例:

@Test
void testThrowException() {
    final Article article = getArticle(null);
    injectRelation.injectRelation(article);
}

执行后,控制台打印如下:

控制台打印信息
2023-03-21 22:26:28req : UserQueryReq(username=null, userId=null, isDeleted=null)

cn.easii.relation.exception.RelationException: java.lang.reflect.InvocationTargetException

	at cn.easii.relation.core.InjectRelation.handleException(InjectRelation.java:105)
	at cn.easii.relation.core.InjectRelation.handleException(InjectRelation.java:108)
	at cn.easii.relation.core.InjectRelation.injectOn(InjectRelation.java:90)
	at cn.easii.relation.core.InjectRelation.lambda$injectRelation$0(InjectRelation.java:58)
	at cn.easii.relation.core.InjectRelation.cachedInvoke(InjectRelation.java:68)
	at cn.easii.relation.core.InjectRelation.injectRelation(InjectRelation.java:58)
	at cn.easii.relation.core.ArticleTest.testThrowException(ArticleTest.java:56)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at cn.easii.relation.core.DataProviderRepository.lambda$registerHandler$0(DataProviderRepository.java:28)
	at cn.easii.relation.core.InjectRelation.inject(InjectRelation.java:152)
	at cn.easii.relation.core.InjectRelation.injectOn(InjectRelation.java:88)
	... 74 more
Caused by: java.lang.IllegalArgumentException: username is empty
	at cn.easii.relation.core.handler.UserInfoDataProvider.getUserByUsername(UserInfoDataProvider.java:17)
	... 81 more

当使用 SpringBoot 环境时,可以在配置文件中配置该默认值,例如可以指定为警告:

easy:
  relation:
    default-exception-strategy: WARN

在 SpringBoot 环境中执行测试方法,控制台打印如下:

控制台打印信息
req = UserQueryReq(username=null, userId=null, isDeleted=null, source=null)
2023-03-21T22:36:33.243+08:00  WARN 14981 --- [           main] cn.easii.relation.core.InjectRelation    : an exception occurred while getting the relation data, error info : java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at cn.easii.relation.core.DataProviderRepository.lambda$registerHandler$0(DataProviderRepository.java:28)
	at cn.easii.relation.core.InjectRelation.inject(InjectRelation.java:152)
	at cn.easii.relation.core.InjectRelation.injectOn(InjectRelation.java:88)
	at cn.easii.relation.core.InjectRelation.lambda$injectRelation$0(InjectRelation.java:58)
	at cn.easii.relation.core.InjectRelation.cachedInvoke(InjectRelation.java:68)
	at cn.easii.relation.core.InjectRelation.injectRelation(InjectRelation.java:58)
	at cn.easii.example.ArticleTest.testThrowException(ArticleTest.java:47)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor