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 中找到。