Graphql-Java实践-2-graphql-java编码的三层架构


上一篇: Graphql-Java实践-1-graphql的理念及quickstart

com.graphql-java-kickstart

在上一篇中我们介绍了graphql的理念,用它开发的好处及quick-start项目,不知道大家有没有发现,我们的quick-start项目除了最核心的graphql-java包还引入了一个辅助开发的包com.graphql-java-kickstart,它提供了对语言和框架的诸多支持,我们的schema文件定义和解析也要依赖它。而它的包下定义的GraphQLResolver也是三层架构的重要一层。关于它的github和文档
https://www.graphql-java-kickstart.com/tools/
https://github.com/graphql-java-kickstart/graphql-java-tools

graphql-java-codegen

另还有一个插件graphql-java-codegen,它可以根据schema中定义的type,input等类型自动生成对应的类信息。我们就不用手工去创建类和schema里的信息相对应了。
github:https://github.com/kobylynskyi/graphql-java-codegen

在我们的项目中除了传统的api application common service dao的分层外,我们新增了graph-model层,用于存放schema中定义的type生成的类,把它单独作为一层就可以被其他层次复用。另外也新增了graph层,用于写graphql的一些逻辑。举例我们的account项目:
在这里插入图片描述

通常开发Java服务会分三层架构,controller service dao,每个层次有自己明确的职责。我们在实践中graphql-java时通常也会分为三层 即:mutation/query resolver dataloader,接下来让我们看看每一层的职责

第一层: mutation/query

如果你已经阅读了上面com.graphql-java-kickstart的文档,你应该知道了query 和 mutation实际上是 Query/Mutation/Subscription是graphql的根对象,虽然他们实际上也是resolver,但是会和其它自定义的resolver不一样。
我们可以简单的理解query中就是所有对外的查询接口的入口,mutation是新增更改删除的接口入口,有点类似controller的职责。

如下所示,在type Query下定义了诸多对外“接口”:
在这里插入图片描述
我们只要新建对应的类,实现GraphQLMutationResolver/GraphQLQueryResolver接口,定义的名称,参数,返回值,对应就好了
在这里插入图片描述

第二层:resolver

有没有发现上面的userInfo在query中的实现特别简单:

    public CompletableFuture<UserInfo> userInfo(Integer uid) {
        return supplyAsync(() -> {
            UserInfo userInfo = new UserInfo();
            userInfo.setUid(uid);
            return userInfo;
        });
    }

只需给Userinfo塞一个值uid就可以了,而实际上userInfo有很多字段,


public class UserInfo {

    private Integer uid;
    private UserBase base;
    private UserExtend userExtend;
    private UserGold gold;
    private Social social;
    private UserMoney money;
    private Collection<CovenantAccountInfo> covenantCompanyRoleAccount;

    public UserInfo() {
    }

那这些字段是怎么取值的呢?这就是resolver的作用了。
通常我们对一个model类定义一个resolver,它要实现GraphQLResolver接口,类里面要根据引子(比如上面的uid)来描述类中所有字段的实现,让我们来看下userinfo的resolver

@Slf4j
@Component
public class UserInfoResolver implements GraphQLResolver<UserInfo> {

    @Autowired
    private CovenantUserDao covenantUserDao;

    UserBase base(UserInfo userInfo) {
        return UserFactory.createUserBase(userInfo.getUid());
    }

    UserExtend userExtend(UserInfo userInfo) {
        return UserFactory.createUserExtend(userInfo.getUid());
    }

    UserGold gold(UserInfo userInfo) {
        return UserFactory.createUserGold(userInfo.getUid());
    }

    Social social(UserInfo userInfo) {
        return UserFactory.createSocial(userInfo.getUid());
    }

    UserMoney money(UserInfo userInfo) {
        return UserFactory.createMoney(userInfo.getUid());
    }

    CompletableFuture<Collection<CovenantAccountInfo>> covenantCompanyRoleAccount(UserInfo userInfo) {
        return CompletableFuture.supplyAsync(() -> {
            Integer uid = userInfo.getUid();
            List<Integer> companyAccountIds = covenantUserDao.selectCompanyAccountIdByUid(uid.longValue());
            if (CollectionUtils.isNotEmpty(companyAccountIds)) {
                return companyAccountIds.stream().map(CovenantAccountFactory::createCovenantAccount)
                        .collect(Collectors.toList());
            }
            return Collections.emptyList();
        });
    }
}

而像base,userExtend的实现逻辑又仅仅是创建一个类,塞一个uid,同样他们也有对应的reslover,

@Slf4j
@Component
public class UserBaseResolver implements GraphQLResolver<UserBase> {

    @Autowired
    @Qualifier("userBaseDataLoader")
    DataLoader<Integer, UserSimple> userBaseDataLoader;
    @Autowired
    @Qualifier("userAvatarDataLoader")
    DataLoader<Integer, UserSimple> userAvatarDataLoader;
    @Autowired
    @Qualifier("userAttrDataLoader")
    DataLoader<Integer, UserAttr> userAttrDataLoader;
    @Autowired
    private UserCentreService userCentreService;

    CompletableFuture<String> userName(UserBase ub) {
        return userBaseDataLoader.loadBy(ub.getUid(), UserSimple::getUserName);
    }

    CompletableFuture<String> mobile(UserBase ub) {
        return userBaseDataLoader.loadBy(ub.getUid(), UserSimple::getMobile);
    }

    CompletableFuture<String> nickName(UserBase ub) {
        return userBaseDataLoader.loadBy(ub.getUid(), UserSimple::getNickName);
    }

    CompletableFuture<String> displayName(UserBase ub) {
        return userBaseDataLoader.loadBy(ub.getUid(), UserSimple::getNickName);
    }
}

有一个小细节不知道大家有没有注意到,在resolver 中字段的对应取值方法有的返回CompletableFuture的包装类型,有的不需返回,这个有什么原则吗。其实就是如果在操作中有去数据库取值的过程或者耗时的io过程,就需要异步取值返回CompletableFuture,如果只是简单的构造下对象,就可以直接返回。这其实和graphql的执行机制有关,我们后面的章节可以细细分析。

第三层:dataloader

上面说到,resolver中关注的是对应类的每个字段的实现,如果每个字段的取值都是自己实现,那必然会有一些问题,比如这个类中的很多字段都来源于同一张表,如果每个字段的实现都是去数据库根据id(引子)执行一遍查询的话,本来我们可以通过一次查询取的,现在reslover在执行的时候每个字段都去查一遍数据库,效率是很低的。于是dataloader登场了

DataLoader<K, V>是graphql-java包下提供的类,泛型K是作为查询引子的值,可以是一个对应的id,也可以是一个bean类型的param。我们将他们注册在spring的Configuration下,实际执行的时候会执行对应的回调逻辑。它通过一定的编排逻辑,保证在同一时刻走到同一个dataloader下的取值逻辑只会被执行一次

下面是我们在项目中封装好的调用dataloader的方法:

    private static <K, V> DataLoader<K, V> createDataLoader(Function<Set<K>, Map<K, V>> fun, CacheMap cacheMap) {
        MappedBatchLoader<K, V> batchLoadFunction = set -> CompletableFuture.supplyAsync(() -> fun.apply(set));
        DataLoaderOptions defaultOptions = DataLoaderOptions.newOptions();
        if (cacheMap != null) {
            defaultOptions.setCacheMap(cacheMap);
        } else {
            defaultOptions.setCachingEnabled(false);
        }
        return DataLoader.newMappedDataLoader(batchLoadFunction, defaultOptions);
    }

通过DataLoader.newMappedDataLoader方法进行构造,需要注意batchLoadFunction的方法也是异步执行的。
另外,Function<Set, Map<K, V>> fun中的k是set类型的,也就是要求我们必须传入一个集合,也就是多个引子。这样保证了如果有多个引子在同一时刻请求,也只需要走一次dataloader。假设是去mysql中取数据,原来不同的param只能去数据库执行两次取值,现在我们可以将他们写在一条sql中取值了。
cacheMap可以传入自己的缓存实现:


    /**
     * 提供一个Function回调以初始化一个可配置基础缓存参数的DataLoader<br/>
     *
     * @param initialCapacity 缓存kv容器初始化大小
     * @param expireSeconds   缓存中数据有效期 单位:s
     */
    public static <K, V> DataLoader<K, V> create(Function<Set<K>, Map<K, V>> fun, int initialCapacity, int expireSeconds) {
        CacheMap cacheMap = new CaffeineCacheMap(initialCapacity, expireSeconds);
        return createDataLoader(fun, cacheMap);
    }

    /**
     * 提供一个Function回调以初始化一个无缓存的DataLoader<br/>
     */
    public static <K, V> DataLoader<K, V> createNoCache(Function<Set<K>, Map<K, V>> fun) {
        return createDataLoader(fun, null);
    }

让我们来看下实际构造的dataloader

package com.onepiece.account.resolver.dataloader;

import com.onepiece.account.user.dao.AccountMoneyDao;
import com.onepiece.account.user.dao.SyncUserDAO;
import com.onepiece.account.user.dao.UserCentreDao;
import com.onepiece.account.user.dao.UserGraphDao;
import com.onepiece.account.user.po.centre.AccountMoney;
import com.onepiece.account.user.po.centre.SocialApple;
import com.onepiece.cache.dataloader.OnePieceDataLoaderUtil;
import org.dataloader.DataLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.stream.Collectors;

@Configuration
public class UserDataLoaderRegister {

    @Autowired
    private UserGraphDao userGraphDao;
    @Autowired
    private SyncUserDAO syncUserDAO;
    @Autowired
    private UserCentreDao userCentreDao;
    @Autowired
    private AccountMoneyDao accountMoneyDao;

    @Bean("userBaseDataLoader")
    DataLoader<Integer, UserSimple> userBaseDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> userGraphDao.findUserPartInfoByUids(uids).stream()
                .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item)));
    }

    @Bean("userAvatarDataLoader")
    DataLoader<Integer, UserSimple> userAvatarDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> userGraphDao.findUserAvatarByUids(uids).stream()
                .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item)));
    }

    @Bean("userAttrDataLoader")
    DataLoader<Integer, UserAttr> userAttrDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> {
            List<Long> longUids = uids.stream().map(uid -> uid.longValue()).collect(Collectors.toList());
            return syncUserDAO.batchGetUserAttr(longUids).stream()
                    .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item));
        });
    }

    @Bean("userExtendDataLoader")
    DataLoader<Integer, UserBase> userExtendDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> {
            List<Long> longUids = uids.stream().map(uid -> uid.longValue()).collect(Collectors.toList());
            return userCentreDao.batchGetUserBase(longUids).stream()
                    .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item));
        });
    }

    @Bean("wechatDataLoader")
    DataLoader<Integer, SocialWechat> wechatDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> {
                    List<Long> longUids = uids.stream().map(uid -> uid.longValue()).collect(Collectors.toList());
                    return userCentreDao.batchGetWechat(longUids).stream()
                            .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item));
                }
        );
    }

    @Bean("qqDataLoader")
    DataLoader<Integer, SocialQq> qqDataLoader() {
        return OnePieceDataLoaderUtil.createNoCache(uids -> userCentreDao.batchGetQq(uids).stream()
                .collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item))
        );
    }
}

可以看到,通过@Configuration @Bean将这些dataloader注入到spring中,实际执行时执行下面的回调。回调中也是batchGet针对批量请求参数的取值!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值