Swift on Server Tour 5 创建 Users

在本章中我们将在数据库中创建 User 表,用于存储用户信息,并理解如何将 Post 表和 User 关联起来,以便我们可以知道每篇微博是由哪个用户编写的。

本章代码可以在 Github 中找到。

设计 User 表的数据结构

一个基础的 User 表应该包含以下字段

  • id:主键,用于唯一标识一个用户
  • username:用户名,用于登录
  • password_hash:密码,用于登录,存储的应该是加密后的密码
  • createdAt:创建时间,用于记录用户创建时间

如果我们插入了一个假数据 happyuser,那么我们的 User 表应该是这样的:

id username password_hash createdAt
0 happyuser encrypted text 2023-1-1

那么如何关联 Post 表呢?

数据库的关系型设计

在关系性数据库中,表关系被总结为三种:

  • 一对一(One-to-One)
  • 一对多(One-to-Many)
  • 多对多(Many-to-Many)

我们的 User 表和 Post 表的关系是一对多,即一个用户可以有多篇微博,而一篇微博只能属于一个用户。

一对多关系的设计

在数据库中,我们可以通过在 Post 表中添加一个 userId 字段来表示这种关系,这个字段用于存储 User 表中的 id,即 Post 表中的每一行都会有一个 userId 字段,用于表示这篇微博是由哪个用户编写的。

修改之前的 Post 表,添加 userId 字段,表示如下:

id userId content createdAt
0 0 这是第一篇博文 2023-1-1
1 0 在 LONCAFE 写代码感觉不错呢! 2023/7/9 14:44

当我们需要查询 happyuser 的所有微博时,只需要在 Post 表中查询 userId = 0 的所有行即可。

创建 User Model

Sources/App/Models 目录下创建 User.swift 文件,添加如下代码:

import Vapor
import Fluent
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "password_hash")
var passwordHash: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: UUID? = nil, username: String, passwordHash: String) {
self.id = id
self.username = username
self.passwordHash = passwordHash
}
}
extension User {
struct CreateDTO: Content {
let username: String
let password: String
}
}

接下来,我们需要添加 User 表的 Migration 文件,用于在数据库中创建 User 表。

Sources/App/Migrations 目录下创建 2_CreateUser.swift 文件,添加如下代码:

import Fluent
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema(User.schema)
.id()
.field("username", .string, .required)
.field("password_hash", .string, .required)
.field("created_at", .datetime)
.create()
}
func revert(on database: Database) async throws {
try await database.schema(User.schema).delete()
}
}

configure.swift 文件中修改 app.migrations.add,用于注册 User Migration:

app.passwords.use(.bcrypt) // Bcrypt 是一种密码加密算法,可以确保多次加密后的密码都是不同的,但是可以通过原始密码验证
app.migrations.add([CreatePost(), CreateUser()])

创建 User Controller

Sources/App/Controllers 目录下创建 UserController.swift 文件,添加如下代码:

import Fluent
import Vapor
struct UserController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.group("users") { posts in
posts.get(use: index)
posts.post(use: create)
}
}
func create(req: Request) async throws -> User {
let postData = try req.content.decode(User.CreateDTO.self)
let user = User(username: postData.username, passwordHash: try await req.password.async.hash(postData.password))
try await user.create(on: req.db)
return user
}
func index(req: Request) async throws -> [User] {
let users = try await User.query(on: req.db).all()
return users
}
}

configure.swift 文件中注册 UserController

try app.register(collection: UserController())

创建 User 的 XCTest 测试用例

Tests/AppTests 目录下创建 UserTests.swift 文件,添加如下代码:

@testable import App
import XCTVapor
final class UserTests: XCTestCase {
var app: Application!
override func setUp() async throws {
app = Application(.testing)
try configure(app)
try await app.autoRevert()
try await app.autoMigrate()
}
override func tearDown() async throws {
app.shutdown()
}
func testCreateUser() async throws {
let userData = User.CreateDTO(username: "happyuser", password: "123456")
try app.test(.POST, "users", beforeRequest: { req in
try req.content.encode(userData)
}, afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let user = try res.content.decode(User.self)
XCTAssertEqual(user.username, userData.username)
XCTAssertTrue(try app.password.verify("123456", created: user.passwordHash))
})
}
func testGetUsers() async throws {
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
try app.test(.GET, "users", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let users = try res.content.decode([User].self)
XCTAssertEqual(users.count, 1)
XCTAssertEqual(users[0].username, user.username)
})
}
}

运行测试用例

在终端中运行 swift test 命令,顺利的话,可以看到测试用例全部通过。

总结

在本章中,我们学习了如何在数据库中创建 User 表,以及如何将 Post 表和 User 表关联起来,但我们尚未真正在数据库中关联这两个表,我们将在下一章中完成这个功能。

本章代码可以在 Github 中找到。