惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

Kevin Blog

产品随想 2 产品力与人才密度 产品随想 1 做营销还是做产品 Play PyTorch Stable Diffusion and ONNX, Ollama on Intel Core Ultra 5 225H Ubuntu 25.04 Play with ROCm, PyTorch, Ollama on Ubuntu 24.04 and 780m 写在新的旅程开始前 想念自然与青春 懒猫微服体验——自由协作的神器 没有光纤的日子怎么上网?自制 Home WI-FI! Swift on Server Tour 6 关联 User 和 Post Swift on Server Tour 5 创建 Users Swift on Server Tour 4 构建 Post Controller Swift on Server Tour 3 构建 Post 的 API 将不懂的日语一拍扫尽,介绍捧读全新的「OCR 工作台」功能 Swift on Server Tour 1 你的第一个 Server App 以及它背后的故事 Swift on Server Tour 0: 为什么这可能是你的好选择 纪念左耳朵耗子 How to learn Japanese by reading Novels and News with the help of Oyomi. Write WebAssembly in Swift and use it in Swift App BenQ WiT ScreenBar Halo 体验报告 记录 2021 年考驾照的体验 捧读的 EPUB 日语轻小说阅读器来了 使用 Go Mobile 开发跨平台 Library 使用 Kotlin Native 开发跨平台 Library 小番茄 - 一个只有陪伴的自习室
Swift on Server Tour 2 连通你的数据库与服务器
2023-07-09 · via Kevin Blog

在本章,我们将设计 Micro Blog 中 Post(帖子)的数据模型,并使用 PostgreSQL 作为我们的数据库来存储内容,在最后,我们会编写一个单元测试,来测试 Post 的创建功能。

.. toc:: Table of Contents :max-level: 3

设计 Post 的数据模型

在我们平时使用的微博系统里,帖子都是由用户发布的,因此作为数据模型的设计,通常的顺序也是先设计用户,然后再设计 Post 的数据模型。

但为了更好的理解如何构建数据之间的关系,我决定这次从 Post 的数据模型开始。

一个简单的 Post 含有以下两个属性

  • content 内容
  • createdAt 创建时间

如果用表格来表示我们的数据,那么看起来就是这样的

content createdAt
这是第一篇博文 2023/7/9 14:42

当发布了更多的数据的时候,表格内容就会变成这样

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

我们最好给每条记录再加上一个不重复的 ID,这样我们就可以通过 ID 来快速准确的表示某一条帖子。

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

Hint

事实上数据在存储在数据库的时候,也正是一个个类似这样的表格

接下来,我们尝试用 Swift 中的 来表示这个数据结构

class Post {
    let id: Int
    let content: String
    let createdAt: Date
}

这样,我们就得到了 Post 这个数据模型最原始的状态。

让 Vapor 认识 Post

现在 Vapor 还不知道如何在数据库里操作 Post 类型的数据,因为我们还有很多信息没有提供给 Vapor,比如:

  1. Vapor 并不知道 Post 这个数据模型与「存储在数据库内的表结构」的对应关系
  2. Vapor 并不知道我们要用的数据库是什么,也不知道如何连接到那个数据库

那么接下来就一步步的解决这些问题

将 Post 写成 Vapor Model

Vapor 使用自己的 Fluent 来完成与数据库的通信,这个功能也被通称为 ORM (Object-relational mapping)

我们修改 Package.swift 来加入 Fluent 的依赖,因为我们不再是一个简单的 HelloWorld 了,因此也顺便把 name 改成 MicroBlog 吧。

// swift-tools-version:5.8
import PackageDescription

let package = Package(
    name: "MicroBlog",
    platforms: [
       .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Fluent", package: "fluent"),
            ]
        ),
    ]
)

接下来,我们创建 Sources/App/Models/Post.swift

import Vapor
import Fluent

final class Post: Model {
    // 数据库中的表名
    static let schema = "posts"

    // 唯一性标识符
    @ID(key: .id)
    var id: UUID?

    // 内容
    @Field(key: "content")
    var content: String

    // 创建时间
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?

    init() { }

    init(id: UUID? = nil, content: String) {
        self.id = id
        self.content = content
    }
}

在 Vapor 中,我们通过一些 Property Wrapper 来辅助完成 Model 和数据库中表的关系映射

  • schema - 是指在数据库存储这个类型的数据的「表的名称」即 posts
  • @ID 是数据在数据库表中的「唯一性标识符」,在 Vapor 中默认推荐使用 UUID 随机字符串来作为 ID,并会默认使用字符串 id 在数据库的表中作为字段名,你可以使用 @ID(custom: "") 来修改这个字段名。
  • @Field 是表示要存储在数据库中的数据属性,通过 @Field(key: "content") 我们显式的声明了 var content: String 对应的是数据库表中 content 这个字段。
  • @Timestamp 是一个特殊的 @Field 类型,专门用来表示存储的是时间,同时带有一个 trigger 功能,在这里我们使用 on: .create 来表示,当 Post 被创建时,自动记录时间。

至此 Vapor 就可以认识 Post 这个数据类型啦。

使用 Docker 启动 PostgreSQL 数据库

我们使用 PostgreSQL 作为我们的数据库服务,直接在电脑上安装 PostgreSQL 是一件比较复杂的事情,通过 Docker 我们可以简化这个过程。

Hint

Docker 是一种轻量级的容器化技术,将 App 运行所需要的运行时封装在一起变成一个沙盒环境,通过这项技术,我们可以在 Linux 系统上无缝的启动其他 App 而不需要在 Host 上安装各种依赖。你可以通过安装 Docker Desktop 来使用这项技术

编写 docker-compose.yml

docker-compos.yml 是容器编排文件,我们在这个文件中描述自己所需要的容器 App 以及其运行的环境变量,网络设置等。

首先在项目根目录里创建 docker-compose.yml ,编写以下内容

version: '3.7'  # 定义 Docker Compose 文件的版本,此处使用的是版本 3.7

volumes:  # 定义卷部分
  db_data:  # docker 会使用这个键作为名字,自动创建 db_data 卷来存储数据

services:  # 定义服务部分

  db:  # db 服务配置
    image: 'postgres:15-alpine'  # 使用 PostgreSQL 15 Alpine 版本的镜像
    volumes:  # 定义挂载卷
      - 'db_data:/var/lib/postgresql/data/pgdata'  # 将 db_data 卷挂载到容器的 /var/lib/postgresql/data/pgdata 目录
    environment:  # 定义环境变量
      PGDATA: '/var/lib/postgresql/data/pgdata'  # 设置 PGDATA 环境变量为 /var/lib/postgresql/data/pgdata
      POSTGRES_USER: 'vapor_username'  # 设置 POSTGRES_USER 环境变量为 vapor_username
      POSTGRES_PASSWORD: 'vapor_password'  # 设置 POSTGRES_PASSWORD 环境变量为 vapor_password
      POSTGRES_DB: 'vapor_database'  # 设置 POSTGRES_DB 环境变量为 vapor_database
    ports:  # 定义端口映射,将主机的 5432 端口映射到容器的 5432 端口
      - '5432:5432'

现在我们在终端中进入 docker-compose.yml 所在的位置,使用 docker-compose up db 命令就可以启动数据库服务了,数据库将在本机的 5432 端口监听。

Caution

如果你看到了这样的错误 Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 请检查是否启动了 Docker Desktop

让 Vapor 连接到数据库

接下来,我们的目标是让 Vapor 获取到我们的服务器信息,连接到我们的服务器。

我们首先修改 Package.swift 增加 Fluent 对 PostgreSQL 的支持

// swift-tools-version:5.8
import PackageDescription

let package = Package(
    name: "MicroBlog",
    platforms: [
       .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
            ]
        ),
    ]
)

随后,在 Sources/App/main.swift 中增加连接数据库的信息

// ...
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
    hostname: "localhost",
    port: 5432,
    username: "vapor_username",
    password: "vapor_password",
    database: "vapor_database",
    tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)

try app.run()

至此,Vapor 便知道如何连接到数据库了,但目前数据库还是一张白纸,并没有建立我们所需要的数据表,因此,在最后我们还需要写一个叫做 Migration 的东西,来更新数据库上的表结构。

使用 Migration 来创建数据库表

Migration 是 Fluent 中用来对数据库表结构进行迁移的功能,接下来我们来了解如何使用这个工具。

Sources/App/Migrations/1_CreatePost.swift 写入以下内容

import Fluent 

// 定义 CreatePost 结构体,实现 AsyncMigration 协议
struct CreatePost: AsyncMigration { 
    // 准备方法,在数据库上进行准备操作
    func prepare(on database: Database) async throws { 
        // 创建 Post 表的数据库模式对象
        try await database.schema(Post.schema)  
            .id()  // 添加 id 列
            .field("content", .string, .required)  // 添加 content 列,类型为字符串,不能为空
            .field("created_at", .datetime)  // 添加 created_at 列,类型为日期时间
            .create()  // 创建 Post 表
    }

    // 回滚方法,在数据库上进行回滚操作
    func revert(on database: Database) async throws { 
        try await database.schema(Post.schema).delete()  // 删除 Post 表的数据库模式对象
    }
}

在运行时,以上的代码会被转换成 SQL 语句,以 prepare 中的代码为例,将会被转换成如下 SQL 代码

CREATE TABLE IF NOT EXISTS public.posts
(
    id uuid NOT NULL,
    content text COLLATE pg_catalog."default" NOT NULL,
    created_at timestamp with time zone,
    CONSTRAINT posts_pkey PRIMARY KEY (id)
)

因此 Migration 也只是 Fluent 这个 ORM 对 SQL 的封装,通过这种封装,可以大幅减少我们编写 SQL 时出错的情况,并能通过常用场景的 SQL 语句优化来提升性能的表现。

如果你需要用到数据库的高级功能的话,Fluent 也支持直接使用 SQL 语句进行 Migration.

注册并执行 Migration

我们需要在 Sources/App/main.swit 中将 CreatePost 注册到 App 中以便一会我们执行 Migration 的时候,App 知道内容是什么

//...
app.migrations.add([CreatePost()])

try app.run()

现在,在项目根目录执行 swift run App migrate ,输入 y 就可以完成数据库中表结构的更新。

The following migration(s) will be prepared:
+ App.CreatePost on default
Would you like to continue?
y/n> y

编写 Unit Test 测试创建 Post

接下来,我们编写 Unit Test 来测试 Post 的创建功能,在 Tests/AppTests/PostTests.swift 中写入以下内容

Hint

编写 Unit Test 可以针对功能进行自动化测试,确保我们服务器的功能不出现异常,我们将在后续章节中继续深入讨论 Unit Test 的使用

@testable import App
import XCTVapor

final class PostTests: XCTestCase {
    func testCreatePost() async throws {
        let app = Application(.testing)
        defer { app.shutdown() }

        // autoRevert 将自动执行所有 Migration 中 revert 的内容
        try await app.autoRevert()
        // autoMigrate 将自动执行所有 Migration 中 prepare 的内容
        // 这两步将重建我们的数据库,为我们提供一个干净的测试环境
        try await app.autoMigrate()

        let post = Post(content: "Hello, world!")
        
        try await post.save(on: app.db)

        let postID = try? post.requireID()
        // 如果 postID 不为 nil 则成功创建,测试通过
        XCTAssertNotNil(postID)
    }
}

随后,修改我们的 Package.swift 文件添加关于 Test 相关的描述

// swift-tools-version:5.8
import PackageDescription

let package = Package(
    name: "MicroBlog",
    platforms: [
       .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
            ]
        ),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

现在,我们可以通过在 Package.swift 所在的路径执行 swift test 来运行单元测试

现在我们会获得一个测试没有通过的提示

FluentKit/Databases.swift:162: Fatal error: No default database configured.
error: Exited with signal code 5

这是因为写在 main.swift 中关于数据库连接的内容并不会在测试中执行,我们需要重构这部分代码,使得两边都可以使用

使用 configure.swift 重构 App 初始化

Sources/App/configure.swift 中写入以下代码

import Fluent
import FluentPostgresDriver
import Vapor

public func configure(_ app: Application) async throws {
    app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
        hostname: "localhost",
        port: 5432,
        username: "vapor_username",
        password: "vapor_password",
        database: "vapor_database",
        tls: .prefer(try .init(configuration: .clientDefault)))
    ), as: .psql)

    app.migrations.add([CreatePost()])
}

修改 main.swift 使用 configure

import Vapor
import Fluent
import FluentPostgresDriver

let app = Application()

app.http.server.configuration.port = 8080

defer { app.shutdown() }

app.get { req async in
    "It works!"
}

try await configure(app)

try app.run()

修改 PostTests.swift 使用 configure

@testable import App
import XCTVapor

final class PostTests: XCTestCase {
    func testCreatePost() async throws {
        let app = Application(.testing)
        defer { app.shutdown() }

        try await configure(app)
        
        // autoRevert 将自动执行所有 Migration 中 revert 的内容
        try await app.autoRevert()
        // autoMigrate 将自动执行所有 Migration 中 prepare 的内容
        // 这两步将重建我们的数据库,为我们提供一个干净的测试环境
        try await app.autoMigrate()

        let post = Post(content: "Hello, world!")
        
        try await post.save(on: app.db)

        let postID = try? post.requireID()
        // 如果 postID 不为 nil 则成功创建,测试通过
        XCTAssertNotNil(postID)
    }
}

现在运行 swift test 我们将会看到测试通过的信息

Test Case '-[AppTests.PostTests testCreatePost]' passed (0.263 seconds).

恭喜你,Vapor 和数据库连通起来了!

本章代码

你可以在 https://github.com/kevinzhow/swift-on-server-tour/tree/main/2 找到本章的相关代码。

拓展:使用 pgAdmin 查看数据库的内容

如果你希望查看数据库里创建了什么内容,使用 pgAdmin 可以连接到 PostgreSQL

Untitled.png

下章预告

在下一个章节,我们将编写 API 来实现 Post 的 CURD(Create Update Read Delete)并进一步学习测试的使用