4.0へのアップグレード¶
このガイドでは、既存のVapor 3.xプロジェクトを4.xにアップグレードする方法を説明します。このガイドでは、Vaporの公式パッケージに加え、よく使用されるプロバイダーについても網羅します。不足している内容があれば、Vaporのチームチャットで質問するのがおすすめです。IssuesやPull Requestも歓迎です。
依存関係¶
Vapor 4を使用するには、Xcode 11.4およびmacOS 10.15以上が必要です。
ドキュメントのインストールセクションで依存関係のインストールについて説明しています。
Package.swift¶
Vapor 4へのアップグレードの最初のステップは、パッケージの依存関係を更新することです。以下は更新されたPackage.swiftファイルの例です。更新されたテンプレートPackage.swiftも確認できます。
-// swift-tools-version:4.0
+// swift-tools-version:5.2
import PackageDescription
let package = Package(
name: "api",
+ platforms: [
+ .macOS(.v10_15),
+ ],
dependencies: [
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
- .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
+ .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
- .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
+ .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
],
targets: [
.target(name: "App", dependencies: [
- "FluentPostgreSQL",
+ .product(name: "Fluent", package: "fluent"),
+ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
- "Vapor",
+ .product(name: "Vapor", package: "vapor"),
- "JWT",
+ .product(name: "JWT", package: "jwt"),
]),
- .target(name: "Run", dependencies: ["App"]),
- .testTarget(name: "AppTests", dependencies: ["App"])
+ .target(name: "Run", dependencies: [
+ .target(name: "App"),
+ ]),
+ .testTarget(name: "AppTests", dependencies: [
+ .target(name: "App"),
+ ])
]
)
Vapor 4向けにアップグレードされたすべてのパッケージは、メジャーバージョン番号が1つ増加します。
Warning
Vapor 4の一部のパッケージはまだ正式にリリースされていないため、-rc
プレリリース識別子が使用されています。
廃止されたパッケージ¶
いくつかのVapor 3パッケージは非推奨となりました:
vapor/auth
: Vaporに含まれるようになりました。vapor/core
: いくつかのモジュールに吸収されました。vapor/crypto
: SwiftCryptoに置き換えられました(Vaporに含まれています)。vapor/multipart
: Vaporに含まれるようになりました。vapor/url-encoded-form
: Vaporに含まれるようになりました。vapor-community/vapor-ext
: Vaporに含まれるようになりました。vapor-community/pagination
: Fluentの一部になりました。IBM-Swift/LoggerAPI
: SwiftLogに置き換えられました。
Fluent依存関係¶
vapor/fluent
は、依存関係リストとターゲットに個別の依存関係として追加する必要があります。すべてのデータベース固有のパッケージには、vapor/fluent
への依存関係を明確にするために-driver
が付けられています。
- .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
+ .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
+ .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
プラットフォーム¶
Vaporのパッケージマニフェストは、macOS 10.15以上を明示的にサポートするようになりました。これにより、あなたのパッケージもプラットフォームサポートを指定する必要があります。
+ platforms: [
+ .macOS(.v10_15),
+ ],
将来的にVaporは追加のサポートプラットフォームを追加する可能性があります。あなたのパッケージは、バージョン番号がVaporの最小バージョン要件以上である限り、これらのプラットフォームの任意のサブセットをサポートできます。
Xcode¶
Vapor 4はXcode 11のネイティブSPMサポートを利用しています。これにより、.xcodeproj
ファイルを生成する必要がなくなりました。Xcodeでプロジェクトのフォルダーを開くと、自動的にSPMが認識され、依存関係が取得されます。
vapor xcode
またはopen Package.swift
を使用して、Xcodeでプロジェクトをネイティブに開くことができます。
Package.swiftを更新したら、Xcodeを閉じてルートディレクトリから以下のフォルダーを削除する必要があるかもしれません:
Package.resolved
.build
.swiftpm
*.xcodeproj
更新されたパッケージが正常に解決されると、コンパイラエラーが表示されるはずです--おそらくかなりの数です。心配しないでください!修正方法をお見せします。
Run¶
最初に行うべきことは、Runモジュールのmain.swift
ファイルを新しい形式に更新することです。
import App
import Vapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()
main.swift
ファイルの内容はAppモジュールのapp.swift
を置き換えるため、そのファイルは削除できます。
App¶
基本的なAppモジュール構造の更新方法を見てみましょう。
configure.swift¶
configure
メソッドはApplication
のインスタンスを受け入れるように変更する必要があります。
- public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
+ public func configure(_ app: Application) throws
以下は更新されたconfigureメソッドの例です。
import Fluent
import FluentSQLiteDriver
import Vapor
// アプリケーションが初期化される前に呼び出されます。
public func configure(_ app: Application) throws {
// `Public/`ディレクトリからファイルを提供
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
// SQLiteデータベースを設定
app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
// マイグレーションを設定
app.migrations.add(CreateTodo())
try routes(app)
}
ルーティング、ミドルウェア、Fluentなどの設定に関する構文の変更は以下で説明します。
boot.swift¶
boot
の内容は、アプリケーションインスタンスを受け入れるようになったため、configure
メソッドに配置できます。
routes.swift¶
routes
メソッドはApplication
のインスタンスを受け入れるように変更する必要があります。
- public func routes(_ router: Router, _ container: Container) throws
+ public func routes(_ app: Application) throws
ルーティング構文の変更に関する詳細は以下で説明します。
サービス¶
Vapor 4のサービスAPIは、サービスの発見と使用を容易にするために簡素化されました。サービスはApplication
とRequest
のメソッドとプロパティとして公開されるようになり、コンパイラがそれらの使用を支援できます。
これをよりよく理解するために、いくつかの例を見てみましょう。
// サーバーのデフォルトポートを8281に変更
- services.register { container -> NIOServerConfig in
- return .default(port: 8281)
- }
+ app.http.server.configuration.port = 8281
NIOServerConfig
をサービスに登録する代わりに、サーバー設定はApplicationの単純なプロパティとして公開され、オーバーライドできます。
// CORSミドルウェアを登録
let corsConfiguration = CORSMiddleware.Configuration(
allowedOrigin: .all,
allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
)
let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
- var middlewares = MiddlewareConfig() // _空の_ミドルウェア設定を作成
- middlewares.use(corsMiddleware)
- services.register(middlewares)
+ app.middleware.use(corsMiddleware)
MiddlewareConfig
を作成してサービスに登録する代わりに、ミドルウェアはApplicationのプロパティとして公開され、追加できます。
// ルートハンドラー内でリクエストを行う。
- try req.make(Client.self).get("https://vapor.codes")
+ req.client.get("https://vapor.codes")
Applicationと同様に、Requestもサービスを単純なプロパティとメソッドとして公開します。ルートクロージャ内では、常にRequest固有のサービスを使用する必要があります。
この新しいサービスパターンは、Vapor 3のContainer
、Service
、およびConfig
タイプを置き換えます。
プロバイダー¶
サードパーティパッケージを設定するためにプロバイダーは必要なくなりました。各パッケージは代わりにApplicationとRequestを新しいプロパティとメソッドで拡張して設定します。
Vapor 4でLeafがどのように設定されるか見てみましょう。
// ビューレンダリングにLeafを使用。
- try services.register(LeafProvider())
- config.prefer(LeafRenderer.self, for: ViewRenderer.self)
+ app.views.use(.leaf)
Leafを設定するには、app.leaf
プロパティを使用します。
// Leafビューキャッシュを無効化。
- services.register { container -> LeafConfig in
- return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
- }
+ app.leaf.cache.isEnabled = false
環境¶
現在の環境(production、developmentなど)はapp.environment
でアクセスできます。
カスタムサービス¶
Vapor 3でService
プロトコルに準拠し、コンテナに登録されていたカスタムサービスは、ApplicationまたはRequestの拡張として表現できるようになりました。
struct MyAPI {
let client: Client
func foo() { ... }
}
- extension MyAPI: Service { }
- services.register { container -> MyAPI in
- return try MyAPI(client: container.make())
- }
+ extension Request {
+ var myAPI: MyAPI {
+ .init(client: self.client)
+ }
+ }
このサービスはmake
の代わりに拡張を使用してアクセスできます。
- try req.make(MyAPI.self).foo()
+ req.myAPI.foo()
カスタムプロバイダー¶
ほとんどのカスタムサービスは、前のセクションで示したように拡張を使用して実装できます。ただし、一部の高度なプロバイダーは、アプリケーションのライフサイクルにフックしたり、保存されたプロパティを使用したりする必要があるかもしれません。
Applicationの新しいLifecycle
ヘルパーを使用してライフサイクルハンドラーを登録できます。
struct PrintHello: LifecycleHandler {
func willBoot(_ app: Application) throws {
print("Hello!")
}
}
app.lifecycle.use(PrintHello())
Applicationに値を保存するには、新しいStorage
ヘルパーを使用できます。
struct MyNumber: StorageKey {
typealias Value = Int
}
app.storage[MyNumber.self] = 5
print(app.storage[MyNumber.self]) // 5
app.storage
へのアクセスは、簡潔なAPIを作成するために設定可能な計算プロパティでラップできます。
extension Application {
var myNumber: Int? {
get { self.storage[MyNumber.self] }
set { self.storage[MyNumber.self] = newValue }
}
}
app.myNumber = 42
print(app.myNumber) // 42
NIO¶
Vapor 4はSwiftNIOの非同期APIを直接公開するようになり、map
やflatMap
のようなメソッドをオーバーロードしたり、EventLoopFuture
のようなタイプをエイリアスしたりしようとしなくなりました。Vapor 3は、SwiftNIOが存在する前にリリースされた初期ベータバージョンとの下位互換性のためにオーバーロードとエイリアスを提供していました。これらは、他のSwiftNIO互換パッケージとの混乱を減らし、SwiftNIOのベストプラクティスの推奨事項により良く従うために削除されました。
非同期の名前変更¶
最も明白な変更は、EventLoopFuture
のFuture
タイプエイリアスが削除されたことです。これは検索と置換で簡単に修正できます。
さらに、NIOはVapor 3が追加したto:
ラベルをサポートしていません。Swift 5.2の改善された型推論により、to:
はそれほど必要ではなくなりました。
- futureA.map(to: String.self) { ... }
+ futureA.map { ... }
newPromise
のようにnew
で始まるメソッドは、Swiftスタイルに合わせてmake
に変更されました。
- let promise = eventLoop.newPromise(String.self)
+ let promise = eventLoop.makePromise(of: String.self)
catchMap
は利用できなくなりましたが、NIOのmapError
やflatMapErrorThrowing
のようなメソッドが代わりに機能します。
複数のフューチャーを組み合わせるためのVapor 3のグローバルflatMap
メソッドは利用できなくなりました。これは、NIOのand
メソッドを使用して多くのフューチャーを組み合わせることで置き換えることができます。
- flatMap(futureA, futureB) { a, b in
+ futureA.and(futureB).flatMap { (a, b) in
// aとbで何かを行う。
}
ByteBuffer¶
以前はData
を使用していた多くのメソッドとプロパティは、NIOのByteBuffer
を使用するようになりました。このタイプは、より強力で高性能なバイトストレージタイプです。APIの詳細については、SwiftNIOのByteBufferドキュメントを参照してください。
ByteBuffer
をData
に戻すには:
Data(buffer.readableBytesView)
map / flatMapのスロー¶
最も難しい変更は、map
とflatMap
がもはやスローできないことです。map
には(やや紛らわしいことに)flatMapThrowing
という名前のスローバージョンがあります。しかし、flatMap
にはスローする対応物がありません。これにより、いくつかの非同期コードの再構築が必要になる場合があります。
スローしないmapは引き続き正常に動作するはずです。
// スローしないmap。
futureA.map { a in
return b
}
スローするmapはflatMapThrowing
に名前を変更する必要があります。
- futureA.map { a in
+ futureA.flatMapThrowing { a in
if ... {
throw SomeError()
} else {
return b
}
}
スローしないflat-mapは引き続き正常に動作するはずです。
// スローしないflatMap。
futureA.flatMap { a in
return futureB
}
flat-map内でエラーをスローする代わりに、フューチャーエラーを返します。エラーが他のスローメソッドから発生する場合、エラーはdo / catchでキャッチしてフューチャーとして返すことができます。
// キャッチしたエラーをフューチャーとして返す。
futureA.flatMap { a in
do {
try doSomething()
return futureB
} catch {
return eventLoop.makeFailedFuture(error)
}
}
スローメソッド呼び出しは、flatMapThrowing
にリファクタリングし、タプルを使用してチェーンすることもできます。
// タプルチェーンを使用してflatMapThrowingにリファクタリングされたスローメソッド。
futureA.flatMapThrowing { a in
try (a, doSomeThing())
}.flatMap { (a, result) in
// resultはdoSomethingの値です。
return futureB
}
ルーティング¶
ルートはApplicationに直接登録されるようになりました。
app.get("hello") { req in
return "Hello, world"
}
これは、ルーターをサービスに登録する必要がなくなったことを意味します。routes
メソッドにアプリケーションを渡してルートを追加し始めるだけです。RoutesBuilder
で利用可能なすべてのメソッドはApplication
で利用可能です。
同期コンテンツ¶
リクエストコンテンツのデコードは同期的になりました。
let payload = try req.content.decode(MyPayload.self)
print(payload) // MyPayload
この動作は、.stream
ボディコレクション戦略を使用してルートを登録することでオーバーライドできます。
app.on(.POST, "streaming", body: .stream) { req in
// リクエストボディは非同期になりました。
req.body.collect().map { buffer in
HTTPStatus.ok
}
}
カンマ区切りのパス¶
一貫性のため、パスはカンマ区切りである必要があり、/
を含んではいけません。
- router.get("v1/users/", "posts", "/comments") { req in
+ app.get("v1", "users", "posts", "comments") { req in
// リクエストを処理。
}
ルートパラメータ¶
Parameter
プロトコルは、明示的に名前付きパラメータを支持して削除されました。これにより、重複するパラメータの問題と、ミドルウェアとルートハンドラーでのパラメータの順不同の取得が防止されます。
- router.get("planets", String.parameter) { req in
- let id = req.parameters.next(String.self)
+ app.get("planets", ":id") { req in
+ let id = req.parameters.get("id")
return "Planet id: \(id)"
}
モデルを使用したルートパラメータの使用については、Fluentセクションで説明します。
ミドルウェア¶
MiddlewareConfig
はMiddlewareConfiguration
に名前が変更され、Applicationのプロパティになりました。app.middleware
を使用してアプリにミドルウェアを追加できます。
let corsMiddleware = CORSMiddleware(configuration: ...)
- var middleware = MiddlewareConfig()
- middleware.use(corsMiddleware)
+ app.middleware.use(corsMiddleware)
- services.register(middlewares)
ミドルウェアはタイプ名で登録できなくなりました。登録する前にミドルウェアを初期化してください。
- middleware.use(ErrorMiddleware.self)
+ app.middleware.use(ErrorMiddleware.default(environment: app.environment))
すべてのデフォルトミドルウェアを削除するには、app.middleware
を空の設定に設定します:
app.middleware = .init()
Fluent¶
FluentのAPIはデータベースに依存しなくなりました。Fluent
だけをインポートできます。
- import FluentMySQL
+ import Fluent
モデル¶
すべてのモデルはModel
プロトコルを使用し、クラスである必要があります。
- struct Planet: MySQLModel {
+ final class Planet: Model {
すべてのフィールドは@Field
または@OptionalField
プロパティラッパーを使用して宣言されます。
+ @Field(key: "name")
var name: String
+ @OptionalField(key: "age")
var age: Int?
モデルのIDは@ID
プロパティラッパーを使用して定義する必要があります。
+ @ID(key: .id)
var id: UUID?
カスタムキーまたはタイプの識別子を使用するモデルは@ID(custom:)
を使用する必要があります。
すべてのモデルは、テーブルまたはコレクション名を静的に定義する必要があります。
final class Planet: Model {
+ static let schema = "Planet"
}
すべてのモデルには空のイニシャライザが必要です。すべてのプロパティがプロパティラッパーを使用するため、これは空にできます。
final class Planet: Model {
+ init() { }
}
モデルのsave
、update
、create
は、モデルインスタンスを返さなくなりました。
- model.save(on: ...)
+ model.save(on: ...).map { model }
モデルはルートパスコンポーネントとして使用できなくなりました。代わりにfind
とreq.parameters.get
を使用してください。
- try req.parameters.next(ServerSize.self)
+ ServerSize.find(req.parameters.get("size"), on: req.db)
+ .unwrap(or: Abort(.notFound))
Model.ID
はModel.IDValue
に名前が変更されました。
モデルのタイムスタンプは@Timestamp
プロパティラッパーを使用して宣言されるようになりました。
- static var createdAtKey: TimestampKey? = \.createdAt
+ @Timestamp(key: "createdAt", on: .create)
var createdAt: Date?
リレーション¶
リレーションはプロパティラッパーを使用して定義されるようになりました。
親リレーションは@Parent
プロパティラッパーを使用し、フィールドプロパティを内部に含みます。@Parent
に渡されるキーは、データベース内の識別子を格納するフィールドの名前である必要があります。
- var serverID: Int
- var server: Parent<App, Server> {
- parent(\.serverID)
- }
+ @Parent(key: "serverID")
+ var server: Server
子リレーションは、関連する@Parent
へのキーパスを持つ@Children
プロパティラッパーを使用します。
- var apps: Children<Server, App> {
- children(\.serverID)
- }
+ @Children(for: \.$server)
+ var apps: [App]
兄弟リレーションは、ピボットモデルへのキーパスを持つ@Siblings
プロパティラッパーを使用します。
- var users: Siblings<Company, User, Permission> {
- siblings()
- }
+ @Siblings(through: Permission.self, from: \.$user, to: \.$company)
+ var companies: [Company]
ピボットは、2つの@Parent
リレーションと0個以上の追加フィールドを持つModel
に準拠する通常のモデルになりました。
クエリ¶
データベースコンテキストは、ルートハンドラー内でreq.db
を介してアクセスされるようになりました。
- Planet.query(on: req)
+ Planet.query(on: req.db)
DatabaseConnectable
はDatabase
に名前が変更されました。
フィールドへのキーパスは、フィールド値の代わりにプロパティラッパーを指定するために$
で始まるようになりました。
- filter(\.foo == ...)
+ filter(\.$foo == ...)
マイグレーション¶
モデルはリフレクションベースの自動マイグレーションをサポートしなくなりました。すべてのマイグレーションは手動で記述する必要があります。
- extension Planet: Migration { }
+ struct CreatePlanet: Migration {
+ ...
+}
マイグレーションは文字列型になり、モデルから切り離されてMigration
プロトコルを使用するようになりました。
- struct CreateGalaxy: <#Database#>Migration {
+ struct CreateGalaxy: Migration {
prepare
およびrevert
メソッドは静的ではなくなりました。
- static func prepare(on conn: <#Database#>Connection) -> Future<Void> {
+ func prepare(on database: Database) -> EventLoopFuture<Void>
スキーマビルダーの作成は、Database
のインスタンスメソッドを介して行われます。
- <#Database#>Database.create(Galaxy.self, on: conn) { builder in
- // ビルダーを使用。
- }
+ var builder = database.schema("Galaxy")
+ // ビルダーを使用。
create
、update
、およびdelete
メソッドは、クエリビルダーと同様にスキーマビルダーで呼び出されるようになりました。
フィールド定義は文字列型になり、次のパターンに従います:
field(<name>, <type>, <constraints>)
以下の例を参照してください。
- builder.field(for: \.name)
+ builder.field("name", .string, .required)
スキーマビルドはクエリビルダーのようにチェーンできるようになりました。
database.schema("Galaxy")
.id()
.field("name", .string, .required)
.create()
Fluent設定¶
DatabasesConfig
はapp.databases
に置き換えられました。
try app.databases.use(.postgres(url: "postgres://..."), as: .psql)
MigrationsConfig
はapp.migrations
に置き換えられました。
app.migrations.use(CreatePlanet(), on: .psql)
リポジトリ¶
Vapor 4でのサービスの動作方法が変更されたため、データベースリポジトリの実装方法も変更されました。UserRepository
のようなプロトコルは引き続き必要ですが、そのプロトコルに準拠するfinal class
を作成する代わりに、struct
を作成する必要があります。
- final class DatabaseUserRepository: UserRepository {
+ struct DatabaseUserRepository: UserRepository {
let database: Database
func all() -> EventLoopFuture<[User]> {
return User.query(on: database).all()
}
}
また、Vapor 4にはもはや存在しないため、ServiceType
への準拠も削除する必要があります。
- extension DatabaseUserRepository {
- static let serviceSupports: [Any.Type] = [Athlete.self]
- static func makeService(for worker: Container) throws -> Self {
- return .init()
- }
- }
代わりにUserRepositoryFactory
を作成する必要があります:
struct UserRepositoryFactory {
var make: ((Request) -> UserRepository)?
mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
self.make = make
}
}
このファクトリーはRequest
に対してUserRepository
を返す責任があります。
次のステップは、ファクトリーを指定するためにApplication
に拡張を追加することです:
extension Application {
private struct UserRepositoryKey: StorageKey {
typealias Value = UserRepositoryFactory
}
var users: UserRepositoryFactory {
get {
self.storage[UserRepositoryKey.self] ?? .init()
}
set {
self.storage[UserRepositoryKey.self] = newValue
}
}
}
Request
内で実際のリポジトリを使用するには、Request
にこの拡張を追加します:
extension Request {
var users: UserRepository {
self.application.users.make!(self)
}
}
最後のステップは、configure.swift
内でファクトリーを指定することです
app.users.use { req in
DatabaseUserRepository(database: req.db)
}
これで、ルートハンドラー内でreq.users.all()
を使用してリポジトリにアクセスでき、テスト内でファクトリーを簡単に置き換えることができます。
テスト内でモックされたリポジトリを使用したい場合は、まずTestUserRepository
を作成します
final class TestUserRepository: UserRepository {
var users: [User]
let eventLoop: EventLoop
init(users: [User] = [], eventLoop: EventLoop) {
self.users = users
self.eventLoop = eventLoop
}
func all() -> EventLoopFuture<[User]> {
eventLoop.makeSuccededFuture(self.users)
}
}
このモックされたリポジトリをテスト内で次のように使用できます:
final class MyTests: XCTestCase {
func test() throws {
let users: [User] = []
app.users.use { TestUserRepository(users: users, eventLoop: $0.eventLoop) }
...
}
}