com.graphql-java-kickstartgraphql-java-codegen第一层: mutation/query第二层:resolver第三层:dataloader
上一篇:
Graphql-Java实践-1-graphql的理念及quickstart
在上一篇中我们介绍了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,它可以根据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接口,定义的名称,参数,返回值,对应就好了
有没有发现上面的userInfo在query中的实现特别简单:
public CompletableFutureuserInfo(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 covenantCompanyRoleAccount;
public UserInfo() {
}
那这些字段是怎么取值的呢?这就是resolver的作用了。
通常我们对一个model类定义一个resolver,它要实现GraphQLResolver接口,类里面要根据引子(比如上面的uid)来描述类中所有字段的实现,让我们来看下userinfo的resolver
@Slf4j @Component public class UserInfoResolver implements GraphQLResolver{ @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 > covenantCompanyRoleAccount(UserInfo userInfo) { return CompletableFuture.supplyAsync(() -> { Integer uid = userInfo.getUid(); List 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{ @Autowired @Qualifier("userbaseDataLoader") DataLoader userbaseDataLoader; @Autowired @Qualifier("userAvatarDataLoader") DataLoader userAvatarDataLoader; @Autowired @Qualifier("userAttrDataLoader") DataLoader userAttrDataLoader; @Autowired private UserCentreService userCentreService; CompletableFuture userName(Userbase ub) { return userbaseDataLoader.loadBy(ub.getUid(), UserSimple::getUserName); } CompletableFuture mobile(Userbase ub) { return userbaseDataLoader.loadBy(ub.getUid(), UserSimple::getMobile); } CompletableFuture nickName(Userbase ub) { return userbaseDataLoader.loadBy(ub.getUid(), UserSimple::getNickName); } CompletableFuture displayName(Userbase ub) { return userbaseDataLoader.loadBy(ub.getUid(), UserSimple::getNickName); } }
有一个小细节不知道大家有没有注意到,在resolver 中字段的对应取值方法有的返回CompletableFuture的包装类型,有的不需返回,这个有什么原则吗。其实就是如果在操作中有去数据库取值的过程或者耗时的io过程,就需要异步取值返回CompletableFuture,如果只是简单的构造下对象,就可以直接返回。这其实和graphql的执行机制有关,我们后面的章节可以细细分析。
第三层:dataloader上面说到,resolver中关注的是对应类的每个字段的实现,如果每个字段的取值都是自己实现,那必然会有一些问题,比如这个类中的很多字段都来源于同一张表,如果每个字段的实现都是去数据库根据id(引子)执行一遍查询的话,本来我们可以通过一次查询取的,现在reslover在执行的时候每个字段都去查一遍数据库,效率是很低的。于是dataloader登场了
DataLoader
下面是我们在项目中封装好的调用dataloader的方法:
private staticDataLoader createDataLoader(Function , Map > fun, CacheMap cacheMap) { MappedBatchLoader 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
cacheMap可以传入自己的缓存实现:
public static DataLoader create(Function, Map> fun, int initialCapacity, int expireSeconds) {
CacheMap cacheMap = new CaffeineCacheMap(initialCapacity, expireSeconds);
return createDataLoader(fun, cacheMap);
}
public static DataLoader createNoCache(Function, Map> 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 userbaseDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> userGraphDao.findUserPartInfoByUids(uids).stream()
.collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item)));
}
@Bean("userAvatarDataLoader")
DataLoader userAvatarDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> userGraphDao.findUserAvatarByUids(uids).stream()
.collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item)));
}
@Bean("userAttrDataLoader")
DataLoader userAttrDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> {
List 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 userExtendDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> {
List 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 wechatDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> {
List 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 qqDataLoader() {
return OnePieceDataLoaderUtil.createNoCache(uids -> userCentreDao.batchGetQq(uids).stream()
.collect(Collectors.toMap(item -> item.getUid().intValue(), item -> item))
);
}
}
可以看到,通过@Configuration @Bean将这些dataloader注入到spring中,实际执行时执行下面的回调。回调中也是batchGet针对批量请求参数的取值!



