Vaporをasync/awaitに書き直してみる（比較する）

YUMEMI Advent Calendar13日目の記事です(!?!?!?!?!?!?)
Valorantが面白すぎて...（言い訳）


## Swiftで非同期処理

Swift5.5から非同期処理の機能としてasync/awaitが追加されました。が、自分は全くキャッチアップできてないので参考にした記事を載せておきます。

https://zenn.dev/akkyie/articles/swift-concurrency



## Vaporでの非同期処理

今までのVaporでもSwiftNIOのEventLoopFutureを使った非同期処理は行われていました。

DBアクセスが1番に挙げられると思いますが、中間テーブルなどを使い出すとコールバック地獄がちょっと大変でした。

```swift
/*-- 番組情報と出演者情報をupsertし、リレーションを張る --*/
let program = upsertProgram(item.program, app.db)
let personalities = item.personalities.map { upsertPersonality($0, app.db) }
return personalities.map {
    $0.flatMap { personality -> EventLoopFuture<Void> in
        program.flatMap { program -> EventLoopFuture<Void> in
            program.$personalities.isAttached(to: personality, on: app.db).flatMap {
                isAttach -> EventLoopFuture<Void> in
                if !isAttach {
                    return program.$personalities.attach(personality, on: app.db)
                }
                return app.eventLoopGroup.future()
            }
        }
    }
}
.flatten(on: app.eventLoopGroup.next())
```

これは流石に他にいい書き方絶対あると思うんですけどね......



EventLoopFutureのasync/await対応はSwiftNIO2.33.0で行われており、

それ以外のVaporの一部の機能のasync/await対応はVapor4.50.0で行われました

https://github.com/vapor/vapor/releases/tag/4.50.0



## 書き直す

今回もagqrの番組表APIを題材にしてます。[公式ドキュメント](https://docs.vapor.codes/4.0/async/)に移行方法があるのでこれを参考にします

まずSwift5.5にあげます

```dockerfile
- FROM swift:5.2-focal-slim
+ FROM swift:5.5-focal-slim
```

VaporとSwiftNIOを更新します（ビルドすればいいので特にやることはない）



### Fluentの書き直し

対応が入っているFluentは結構簡単です。

```swift
/// 開始時間と終了時間を基準に(unique)insert or updateを行う.
func upsertProgram(_ program: Program, _ db: Database) -> EventLoopFuture<Program> {
    return
        Program
        .query(on: db)
        .filter(\.$startDatetime == program.startDatetime)
        .filter(\.$endDatetime == program.endDatetime)
        .first()
        .flatMap { dbProgram -> EventLoopFuture<Program> in
            program.id = dbProgram?.id
            // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
            program._$id.exists = dbProgram?.id != nil
            return program.save(on: db).transform(to: program)
        }
}
```

```swift
/// 開始時間と終了時間を基準に(unique)insert or updateを行う.
func upsertProgram(_ program: Program, _ db: Database) async throws -> Program {
    let dbProgram = try? await Program.query(on: db)
        .filter(\.$startDatetime == program.startDatetime)
        .filter(\.$endDatetime == program.endDatetime)
        .first()

    program.id = dbProgram?.id
    // idを挿入するだけだとうまくupdateできなかったため、判定要素を上書きする
    program._$id.exists = dbProgram != nil

    try await program.save(on: db)
    return program
}
```

単純にネストが減る感じ



ちなみにさっきの大変なやつもスッキリ書けます。

```swift
let insertedProgram = try await upsertProgram(programGuide.program, app.db)
var insertedPersonalities: [Personality] = []
for personality in programGuide.personalities {
    let p = try await upsertPersonality(personality, app.db)
    insertedPersonalities.append(p)
}
for personality in insertedPersonalities {
    let isAttached = try? await insertedProgram.$personalities.isAttached(to: personality, on: app.db)
    if isAttached == false {
        try await insertedProgram.$personalities.attach(personality, on: app.db)
    }
}
```



### 未対応のVapor定義オブジェクトを書き直す

[リリースノート](https://github.com/vapor/vapor/releases/tag/4.50.0)にある通り、

- Client
- RoutesBuilder
- ResponseEncodable
- Responder
- Middleware
- AsyncPasswordHasher
- Cache
- View
- ViewRenderer

についてはasync/awaitに対応しているので簡単に（上のように）書き直せますが、これ以外のは少し手間がかかります。

公式にもありますが、EventLoopFutureからasyncへ・asyncからEventLoopFutureへ書き換える必要があります。

```swift
// EventLoopFuture -> async
let futureResult = try await someMethodThatReturnsAFuture().get()
// async -> EventLoopFuture
let promise = request.eventLoop.makePromise(of: String.self)
promise.completeWithTask {
    try await someAsyncFunctionThatGetsAString()
}
let futureString: EventLoopFuture<String> = promise.futureResult
```

ScheduledJobの場合 `func run(context: QueueContext) -> EventLoopFuture<Void>` を要求しているので 、async用`func asyncRun(context: QueueContext) async -> Void` を定義してあげることで書き換えました。

```swift
func run(context: QueueContext) -> EventLoopFuture<Void> {
    // fetchWeeklyの返り値はEventLoopFuture<[Data?]>
    return client.fetchWeekly(app: context.application).flatMap { responses -> EventLoopFuture<Void> in
        responses
            .map { response -> EventLoopFuture<Void> in
                guard let response = response else {
                    return context.application.eventLoopGroup.future(error: "番組表データがありませんでした。")
                }
                let programGuide = self.parser.parse(response)
                return self.repository.save(programGuide, app: context.application)
            }
            .flatten(on: context.application.eventLoopGroup.next())
    }
}
```

```swift
func run(context: QueueContext) -> EventLoopFuture<Void> {
    let promise = context.eventLoop.makePromise(of: Void.self)
    promise.completeWithTask {
        await self.asyncRun(context: context)
    }
    return promise.futureResult

}

func asyncRun(context: QueueContext) async -> Void {
    // fetchWeeklyは async -> [Data?]
    let responses = await client.fetchWeekly(app: context.application)
    for response in responses {
        guard let response = response else {
            print("番組表データがありません。")
            continue
        }
        let programGuide = self.parser.parse(response)
        await self.repository.save(programGuide, app: context.application)
    }
}
```



## 感想

めちゃ書きやすい、最高。

throwにtryが必須なようにasyncにはawaitが必須になっているため、jsのasyncとはイメージが少し違った。

arrayの各要素に対してasyncな処理を加えるみたいな書き方をしたい時に、単純にmapだとダメでtaskを使わないといけなかったり、forのが綺麗だったりが結構引っかかりポイントだった。



アドベントカレンダー1週間遅れガチ謝罪、アドベントカレンダーは [こちら](https://qiita.com/advent-calendar/2021/yumemi)																																																														

ちなみに書き直した時のPRはこれです -> https://github.com/sun-yryr/agqr-program-guide/pull/16