Eisen's Blog

© 2024. All rights reserved.

Github Actions tips 更新

2021 November-01

workflow 的重用

分两部分,直接调用和模板。

直接调用

https://docs.github.com/en/actions/learn-github-actions/reusing-workflows

重点如下:

  1. 允许被其他调用的 workflow 必须明确标注可触发事件 workflow_call
  2. 不能套娃,这个 workflow 不能再掉用其他 workflow
  3. 对调用有一定限制,具体看文档

使用场景我觉得是两个:

  1. 多仓库协同的部署,比如 a 必须依赖 b,那么 a 跑完就跑 b
  2. 有些 workflow 每个地方都用,可以重用,不过感觉这部分更适合模板?

模板

https://docs.github.com/en/actions/learn-github-actions/creating-workflow-templates

只有 org 下能用,必须放到 .github 这个仓库里。放进来的 workflow 在其他仓库创建 workflow 的时候会出现,方便复制。

github action 控制日志和关键信息展示

https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions

分组日志

https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines

2021 11 02 00 17 16

重点展示

2021 11 02 00 17 49

这个东西非常适合把构建过程中比较重要的东西展示出来,比如我就用它来展示构建生成的镜像的 tag:

2021 11 02 00 18 31


GraphQL 测试工具梳理

2021 October-21

目前 Postman 在我们团队的使用情况

这部分工作主要是对标之前在 REST 场景下使用 Postman 测试的工作。在 REST 的场景下,Postman 是一个很好的工具,简单介绍下我们使用到的一些功能:

  1. Collections 可以将一系列的 Request 聚合在一起很好的规整起来,当需要测试某个接口的问题时,搜索 Collection 执行具体的 Request 即可。
  2. Environments 维护一系列变量,点击切换可以测试不同的环境。

Environments
Environments

  1. Tests 虽然可以做测试,但实际上就是在跑完一个 request 之后可以执行一段 JS 脚本,这个脚本不一定都是测试的内容,比如我可以在 login 之后把 response 中返回的 token 存储为一个 Postman 的 variable,然后在后面的请求中使用这个 variable 避免重复登录。

Login 后的执行脚本
Login 后的执行脚本

  1. 组织协作,可以让团队里的人都共享 Collections 避免了很多重复劳动和沟通。
  2. Monitors 这部分用的不是很多,主要是确定几个环境的服务没什么问题。

在这个视频 Postman Beginner's Course - API Testing 中介绍了很多 Postman 的功能,很有帮助。

对比 Postman 和 GraphiQL 以及 Apollo Studio 的功能

不过很遗憾 Postman 在 GraphQL 场景的支持似乎不是很积极,目前仅仅做了简单的支持。但考虑到上文中 Postman 的那些功能在 GraphQL 下依然有不可替代的使用价值,这里我先罗列在 GraphQL 下几个我们关心的功能在几个工具里的支持情况。

请求页面的自动补全

如果说 REST API 是散装弱类型 API 那么 GraphQL 就是强类型的 API 了,提交的内容必须要和 schema 兼容。在 GraphiQL(对,多了个 i)这样的工具里,你可以在右侧看到目前的 schema 并且在写 GQL 的时候可以自动提示,可以很方面的查询自己想要的字段和类型。

Ctrl / Cmd + 鼠标左键点击类型时右侧会展示对应的类型
Ctrl / Cmd + 鼠标左键点击类型时右侧会展示对应的类型

而在 Apollo Studio 里则可以像如下图这样直接在文档里点击字段和类型拼接查询请求,也很方便。

左侧搜索具体的操作后可以直接勾选属性拼接查询 Query
左侧搜索具体的操作后可以直接勾选属性拼接查询 Query

如果你在 GraphQL 里提供了字段或者方法的注释,Apollo Studio 或者是 GraphiQL 也都会在这些页面给你展示出来。

Postman 目前就只有自动补全,注意这个自动补全的意思是你必须先写点什么,然后我给你不足后面的内容,如果你什么都没写,那不好意思,我也什么不会给你提供的,没有文档,没有提示…

Postman GraphQL 请求
Postman GraphQL 请求

Schema 更新手段

既然是强类型的 schema 那么 schema 的更新就很重要了。我们所使用的 GraphiQL 目前是 DGS framework 自己带的,是通过 GraphQL 的 instropection 获取的,自然就是最新的了。不过这种功能只能在开发环境使用,在生产环境肯定是需要把这个功能关掉的,不然别人随随便便就拿到了你全部的 schema 感觉有点不对劲。GraphiQL 应该也是有那种主动提供 schema 做独立部署的方式的吧,主要是目前 dev 环境开放我们也能接受就没有做更多的调研了。

Apollo Studio 提供了一个名为 Rover 的工具,可以有多种方式将 schema 提交给 Apollo。可以通过 CI/CD 流程去自动化这个过程,后面会提到。

Postman 和 Apollo Studio 的方式类似,只是它没有自己的工具,需要通过 Postman 自身的 API 去拼接请求,并且 API 中各种 ID 的查找显得略微晦涩了,麻烦一些,但也能走通。不过在尝试的过程中发现 Postman 中的 GraphQL schema 对 GraphQL 的扩展语法并不支持,无法识别像 extend 这样的关键词,因此不支持那种把 schema 拆分成多个文件通过 cat 到一起提交的简单方式。

Schema 变更日志

这部分算是 Apollo 的特色了,满分。并且找不到完美的替代方案,我找到的一个开源方案仅仅是支持 schema 以文本形式的 diff 而已,并没有做 schema 解析后的对比。

2021 10 21 19 29 13

Postman 就别想了。

还有另外一个工具 GraphQL Playground 虽然很好用,Apollo Server 也做了集成,但未来的发展比较模糊,应该会考虑和 GraphiQL 这个项目做合并,就不讲了。

目前的方案

综上所述 Postman 在 GraphQL 的支持只有 schema 以及针对 schema 的自动补全(写 GraphQL 的那个框框甚至连格式化都没有)。但我依然觉得 Collections Environments Tests 这些功能无法被其他平台替代。因此就只好同时使用 Apollo Studio 和 Postman

  1. Apollo Studio 使用其 Explorer 和 Schema ChangeLog 的功能,每次在 Explorer 写好了 Query 之后就粘贴到 Postman 里,创建一个对应的 Request,方便未来重用。当然,也可以补充一些测试脚本。
  2. 通过 Github Action 将最新的 schema 更新到 Apollo Studio 和 Postman 中,以下是用到的 workflow:
name: GraphQL workflow

on:
  workflow_dispatch:
  push:
    branches:
      - master

jobs:
  sync:
    runs-on: ubuntu-latest
    environment: dev
    env:
      APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
      GRAPHQL_ENDPOINT: ${{ secrets.GRAPHQL_ENDPOINT }}
    steps:
      - uses: actions/checkout@v2
      - name: Install Rover                                                    # [1]
        run: |
          curl -sSL https://rover.apollo.dev/nix/latest | sh
          echo "$HOME/.rover/bin" >> $GITHUB_PATH
      - name: Upload dev schema for apollo                                     # [2]
        run: |
          ls ./src/main/resources/schema/*.graphqls | xargs cat - | rover graph publish openbayes@dev --schema -
      - name: Generate schema                                                  # [3]
        run: |
          rover graph introspect $GRAPHQL_ENDPOINT > schema.graphql
      - name: Update Schema from File                                          # [4]
        uses: aisensiy/update-postman-schema-action@master
        with:
          postman-key: ${{ secrets.POSTMAN_KEY }}
          postman-api-id: "xx"
          postman-api-version: "xx"
          postman-api-schema-id: "xxx"
          schema-filepath: ./schema.graphql

针对这个 workflow 我针对注释的 4 个点做一些解释:

  1. 我们用到了 Rover 这个工具,按照其文档进行安装
  2. 对于 Apollo Studio 我们可以把所有的 schema cat 一起后提交
  3. 而正如上文所述,对于 Postman 只能提交一个格式比较标准的 schema 这里就直接用了 Rover 的 introspect 功能,不过更好的办法应该是用脚本正则化 schema 然后提交
  4. Postman 的 API 没有很方便的提交方式,就之后做了一个简单的 Github Actions 功能,把 schema 更新到 Postman 中。

进一步的工作

  1. 尝试有没有其他工具直接输出一个标准的、合并了的 schema 提交给 Postman 和 Apollo Studio
  2. Postman 似乎也可以和 GitHub 集成将 Collections 持久化到 GitHub 的一个仓库里
  3. 在 CI 里更新 schema 其实并不是最佳的时机,因为这个时候环境实际上还没有做部署,所以应该考虑如何让 CI 直接控制 CD 流程,保证环境中的 schema 和 Postman / Apollo Studio 中的 schema 同步

对 CQRS 和 DDD 的一些新的讨论

2021 October-18

some tips for ddd 里讲明白了 DDD 是针对 写模型 的,而对于 读模型 则可以通过一层薄薄的 Query Service 直接从数据库拼装 DTO 实现,这样子的好处是:

  1. 遵守了 DDD 中 aggregate 的访问原则 / 持久化原则,不会因为一个「展示包含用户头像的文章列表」而被轻易的破坏
  2. 减少了在 读模型DTO 从 DDD 的 Repository 里各种转换和拼接,也避免了在 Repository 里面提供分页之类的奇怪方法

在一个简单的项目 Spring Boot + Spring MVC + MyBatis 版本的 Real World 实现 里,这个思路也得到了充分的体现。并且在这个过程中没有遇到什么冲突的地方。

不过最近一两年做的 openbayes 项目里,我发现 cqrs 的原则依然遇到了一些和 DDD 相冲突的地方,这里我

  1. 首先记录一些最近补充学习的一些与该主题相关的资料,一方面确认之前的理解没有严重的偏差,另一方面引入一些相关的讨论
  2. 然后结合具体的业务去描述遇到的问题
  3. 至于能否解决后面再说...

有关 ddd / cqrs 的一些资料更新

首先就是我最近看的一些内容。看了很多内容,包括 Martin Folwer 的一些链接,感觉最终将的比较清楚的还是 greg young 的内容。

CQRS 是由 greg young 提出的,他有一篇名为 CQRS Documents by Greg Young 的资料比较详尽的探讨了 CQRS 和 event-sourcing 到底要解决什么样子的问题。开篇提出在我们所构建的系统不再是 CRUD 之后,可以引入 Command 的概念,描述 Task Based User Interface。而在处理这些 Command 的时候,会发现读和写流程处理会有非常大的区别,并且如果采用同一套 Domain Object 会引入如下问题:

  1. Large numbers of read methods on repositories often also including paging or sorting information. 在 Repository 中有大量的读操作需要包含分页 / 排序相关的功能。
  2. Getters exposing the internal state of domain objects in order to build DTOs. 为了组装 DTO 不得不暴露很多 Domain Object 的内部状态。
  3. Use of prefetch paths on the read use cases as they require more data to be loaded by the ORM. 由于读模型需要更多的数据,所以需要在 ORM 中组织额外的数据加载。
  4. Loading of multiple aggregates to build a DTO causes non-optimal querying to the data model. Alternatively aggregate boundaries can be confused because of the DTO building operations. 在组装 DTO 时,为了获取多个聚合会容易出现低效的查询语句。另外,组装聚合的边界也会因为 DTO 的组装而被打破。

可以看到要解决的痛点和我之前的理解没有什么偏差。

通过分别对读写构建独立的流程可以很好的缓解以上问题:

Separated Data Models with CQRS
Separated Data Models with CQRS

后续有关 event-sourcing 的内容就不再介绍了,我个人认为 event-sourcing 有关 data storage 的修改过于复杂,但其思想(比如采用反向的 Domain Event)确实已经被很好的采用了(openbayes 在子系统之间做数据同步也采用了类似的理念)。这部分以后单独讨论。

另外我看到了大神的 如何落地业务建模 虽然没有明确的指出 CQRS 的问题,但却在「跨越现实的障碍」章节提出了类似于

user.getSubscriptions().subList(from, from + pageSize)

的思路,描述的是一种统一的模型,而不是读写分离的模型。这的确是一个非常好的思路,通过依赖注入一个 Query Service 到具体的 Domain Object 可以很好的解决读写用例放在一起不协调的问题。这也让我开始重新思考 CQRS 是不是有一些问题。

CQRS 遇到的问题

首先,我觉得最大的问题就在于 CQRS 一定程度上让 读模型 成了二等公民。写模型 很重要,需要很仔细的遵循设计思路,而 读模型 就随便糊弄糊弄就完事了...

然后,其实 读模型 很多时候也不是纯粹的获取 DTO 做拼接就完事了。在涉及到权限的时候,DTO 也需要依据当前用户对数据做裁剪并依据对应的权限以及当前对象的状态补充对应的权限属性。

举一个例子,在 openbayes 里有一个 Job 的概念,它有自身的状态(CREATED RUNNING CANCELLING ...),并且对应着一个状态机。在 GET /jobs/{jobid} 时,不同的状态下会返回对应的 links 表明当前用户可以对该 Job 做什么样子的操作。比如处于 RUNNING 状态的 Job 对于有权限的用户应该有如下的 links:

{
  ...
  links: {
    "stop": "<link>",
    "create-snapshot": "<link>",
    "open": "<link>"
  }
}

这些 links 会为前端提供线索用于展示不同的交互 UI。

也就是说,从数据库里拿出来的 DTO 依然需要走一轮业务支持才能使用,没有想象中的那么傻瓜。并且有关状态机的逻辑已经在对应的 Domain Object 中写了一遍了,但是在返回对应的 JSON 数据的 REST 层依然存在大量的代码用于处理这些内容。

可能的解决方案

合并模型

第一个方案自然是不再做 CQRS 了,而应该统一模型,按照 如何落地业务建模 的思路将两个模型的内容引入到同一个模型中去。不过这似乎是一个工作量不小的事情,很少有人会下定决心这么去做的,以及目前没有很健全的例子,需要先做小规模尝试,最终一定会花更多的时间...

不过不得不说 GraphQL 的引入似乎是一个很好的契机。

GraphQL 强调整个暴露的 API 是一张大图,而具体的属性的获取是一个渐进的过程。还是用 Real World 项目举例:

{
  user(id: "eisenxu") {
    articles {
      id
      title
      content
      comments {
        id
        content
        author {
          id
          name
        }
      }
    }
  }
}

这里是一个比较复杂的查询,获取了一个用户的文章列表以及每一篇文章的下的评论,以及每一篇评论的作者。对应的代码大概会是这个样子:

class ArticleDatafetcher {
  public List<Article> fetchArticles(User user) {
    return user.getArticles();
  }
}

class CommentDatafetcher {
  public List<Comment> fetchComments(Article article) {
    return article.getComments();
  }
}

class UserDatafetcher {
  public User fetchUser(User user) {
    return user;
  }

  public User fetchCommentAuthor(Comment comment) {
    return comment.getAuthor();
  }
}

每一个 Entity 或者 Entity 集合属性的获取都会对应一个 上文 也就是它是从那里获取的:

  • user(id: 'eisenxu'): 上文 为默认的 Query
  • articles: 上文user
  • comments: 上文article
  • author: 上文comment(当然也会有 上文articleauthor

如果同样是 REST 的接口,那么就需要把 userarticle 全部返回,这样的话就需要把 CommentDatafetcherUserDatafetcher 的工作全部放到 QueryService 中去。但是 GraphQL 则像是在按需获取:如果你要了 comments 就会调用 CommentDatafetcher.fetchComments 否则就不会执行。这样子就让上文中那种 user.getSubscriptions().subList(from, from + pageSize) 的方式变得更加的顺理成章了。

抽取公共逻辑

刚好最近 ddd/cqrs 的 google group 也在讨论这个主题 Read model business logic duplication。这里给出的方案也非常简单:

  1. 如果这种权限展示逻辑只在 read model 里,那干脆就放 read model 好了,没什么大不了
  2. 如果两边都出现了,那就抽取 Speicification 让两边共享
  3. 对于一些场景,可以让 write model 直接去调用 read model 使用其内容

打补丁自然是简单不少,不过总感觉不是那么舒服...这部分我也就只能写到这里了,虽然写的不多,可却是花了不少时间去想这坨东西...现在回看目前 openbayes 的代码还是有不少的问题,后续再继续理一理吧。