在上一篇中我们介绍了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,接下来让我们看看每一层的职责
如果你已经阅读了上面com.graphql-java-kickstart的文档,你应该知道了query 和 mutation实际上是 Query/Mutation/Subscription是graphql的根对象,虽然他们实际上也是resolver,但是会和其它自定义的resolver不一样。
我们可以简单的理解query中就是所有对外的查询接口的入口,mutation是新增更改删除的接口入口,有点类似controller的职责。
如下所示,在type Query下定义了诸多对外“接口”:
我们只要新建对应的类,实现GraphQLMutationResolver/GraphQLQueryResolver接口,定义的名称,参数,返回值,对应就好了
有没有发现上面的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的执行机制有关,我们后面的章节可以细细分析。
上面说到,resolver中关注的是对应类的每个字段的实现,如果每个字段的取值都是自己实现,那必然会有一些问题,比如这个类中的很多字段都来源于同一张表,如果每个字段的实现都是去数据库根据id(引子)执行一遍查询的话,本来我们可以通过一次查询取的,现在reslover在执行的时候每个字段都去查一遍数据库,效率是很低的。于是dataloader登场了
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
cacheMap可以传入自己的缓存实现:
/**
* 提供一个Function回调以初始化一个可配置基础缓存参数的DataLoader
*
* @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
*/
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针对批量请求参数的取值!