Kitura

APIDoc Build Status - Master macOS Linux Apache 2 Slack Status

Swift-Kuery

Swift-Kuery 是一个可插拔的 SQL 数据库驱动/SDK 抽象层。它的主要思想是统一各种关系数据库的 API,提供一个 Swifty 风格但又类似 SQL 的 API。这使得在数据库之间轻松切换,并为对象关系映射 (ORM) 框架奠定基础。

Swift-Kuery-ORM 是一个构建于 Swift-Kuery 之上的 ORM,它允许您简化模型对象在服务器上的持久化。

Swift-Kuery 是一个易于学习、易于使用的框架,它带有一组 已实现的插件

目录

Swift 版本

最新版本的 Swift-Kuery 需要 Swift 4.0 或更高版本。您可以通过点击此链接下载此版本的 Swift 二进制文件。不保证与其他 Swift 版本的兼容性。

用法

此示例演示了如何使用 Swift-KuerySwift-Kuery-PostgreSQL 插件执行 SQL 查询。

此示例的起点是一个现有的 Swift 包。如果您还没有,请创建一个目录并进入该目录,例如 SwiftKueryExample。现在运行 swift package 的 init 命令,创建一个可执行类型,通过运行 swift package init --type executable

创建 PostgreSQL 数据库

  1. 安装 PostgreSQL

Mac

brew install postgresql

Ubuntu Linux

sudo apt-get install postgresql postgresql-contrib

  1. 创建一个名为 school 的数据库

    createdb school
    psql school
    
  2. 创建一个名为 grades 的表

    CREATE TABLE "Grades" (
        id varchar(100) PRIMARY KEY,
        course text NOT NULL,
        grade integer
    );  
    

更新您的 Package.swift 文件

将 Swift-Kuery 和您的 Kuery 插件(在本例中为 Swift-Kuery-PostgreSQL)添加到应用程序 Package.swift 文件中的 dependencies 中。将 "x.x.x" 替换为最新的 Swift-Kuery 版本,将 "y.y.y" 替换为最新的插件 版本

dependencies: [
    ...
    // Add this line
    .package(url: "https://github.com/Kitura/Swift-Kuery.git", from: "x.x.x"),
    .package(url: "https://github.com/Kitura/Swift-Kuery-PostgreSQL.git", from: "y.y.y"),
  ],
  targets: [
    .target(
      name: ...
      // Add the module to your target(s)
      dependencies: [..., "SwiftKuery", "SwiftKueryPostgreSQL"]),
  ]

执行 SQL 查询

main.swift 文件内部

  1. 将 SwiftKuery 和 SwiftKueryPostgreSQL 添加到您的 import 语句中
import SwiftKuery
import SwiftKueryPostgreSQL
  1. 创建一个 Table 类,它与您在数据库中创建的 grades 表匹配
class Grades: Table {
    let tableName = "Grades"
    let id = Column("id", Int32.self, primaryKey: true)
    let course = Column("course", String.self)
    let grade = Column("grade", Int32.self)
}
let grades = Grades()
  1. 创建到 PostgreSQL 的连接池
let pool = PostgreSQLConnection.createPool(host: "localhost", port: 5432, options: [.databaseName("school")], poolOptions: ConnectionPoolOptions(initialCapacity: 10, maxCapacity: 50))
  1. 创建一些学生示例
let students: [[Any]] = [[0, "computing", 92], [1, "physics", 75], [2, "history", 83]]
  1. 连接到数据库并执行 SQL 查询
pool.getConnection() { connection, error in
    guard let connection = connection else {
        guard let error = error else {
            return print("Unknown error")
        }
        return print("Error when getting connection from pool: \(error.localizedDescription)")
    }
    let insertQuery = Insert(into: grades, rows: students)
    connection.execute(query: insertQuery) { insertResult in
        connection.execute(query: Select(from: grades)) { selectResult in
            guard let resultSet = selectResult.asResultSet else {
                return print("No result set returned from query")
            }
            resultSet.forEach() { row, error in
                guard let row = row else {
                    guard let error = error else {
                        // Processed all results
                        return
                    }
                    // Handle error
                    return
                }
                guard row.count == 3 else {
                    // Expecting three elements per row
                    return print("Row has wrong number of elements. Expecting 3, returned: \(row.count)")
                }
                print("Student \(row[0] ?? ""), studying \(row[1] ?? ""), scored \(row[2] ?? "")")
            }
        }
    }
}
  1. 如果您现在运行应用程序,它会立即执行,因为 SwiftKuery API 是异步行为的。对于这个简单的可执行文件,您可以添加一个 Dispatch Semaphore 来强制应用程序等待异步回调完成
// Add the following after the existing imports:
import Dispatch
let waitSemaphore = DispatchSemaphore(value: 0)

// Update the forEach callback to look like:
resultSet.forEach() { row, error in
    guard let row = row else {
        // Processed all results
        waitSemaphore.signal()
        return
    }
    print("Student \(row[0] ?? ""), studying \(row[1] ?? ""), scored \(row[2] ?? "")")
}

// Add the following line at the end of the main.swift file
waitSemaphore.wait()
  1. 保存 main.swift 文件。运行 swift build 来构建可执行文件。
  2. 运行可执行文件 .build/debug/<yourPackageName>.

这将打印每个学生的 idcoursegrade,这些数据是从数据库中查询出来的

Student 0, studying computing, scored 92
Student 1, studying physics, scored 75
Student 2, studying history, scored 83

如果您使用 psql school 进入您的数据库并输入 TABLE grades;,您可以看到该表已填充了学生数据。

使用参数化防止 SQL 注入

在动态查询中使用未经消毒的数据是 SQL 注入漏洞最常见的起因之一。参数化查询可以帮助防止 SQL 注入攻击。

如果 supplied_key1supplied_key2 包含不受信任的数据(即,未经验证的数据),则以下代码容易受到 SQL 注入攻击

let query = Select(from: confidential)
  .where(confidential.key1 == supplied_key1 || confidential.key2 == supplied_key2)

connection.execute(query: query) { queryResult in
  ...
}

为了防止 SQL 注入攻击,请使用以下参数化版本的代码

let query = Select(from: confidential)
  .where(confidential.key1 == Parameter() || confidential.key2 == Parameter())

connection.execute(query: query, parameters: supplied_key1, supplied_key2) { queryResult in
  ...
}

预处理语句

如果您的应用程序重复执行相同(或相似)的 SQL 语句,只是参数不同,您可以通过使用预处理语句来提高应用程序的性能。预处理语句可以减少解析时间,因为数据库仅解析和编译语句模板一次,然后存储结果但不执行它。稍后,应用程序为语句模板的参数提供值,数据库执行该语句。

例如,假设我们的应用程序需要检索平均成绩高于给定值的课程的平均成绩;我们希望改变这个值。让我们更改查询以使用参数而不是预定义的值

let query = Select(grades.course, round(avg(grades.grade), to: 1).as("average"), from: grades)
            .group(by: grades.course)
            .having(avg(grades.grade) > Parameter())
            .order(by: .ASC(avg(grades.grade)))

现在,准备语句并根据需要使用不同的参数值执行多次。使用 release 函数释放预处理语句

connection.prepareStatement(query) { result in
    guard let statement = result.asPreparedStatement else {
        // Handle error
        return
    }
    // Execute the statement
    connection.execute(preparedStatement: preparedStatement, parameters: [70]) { result in
        ...
        connection.execute(preparedStatement: preparedStatement, parameters: [25]) { result in
            ...
            connection.release(preparedStatement: preparedStatement) { result in
                ...
            }
        }
    }
}

注意preparedStatement 是预处理语句的插件特定句柄。

模式管理

表创建

Swift-Kuery 使您能够在数据库服务器上创建表。

让我们回顾一下我们在上面的示例中使用的 Grades 表

class Grades: Table {
    let tableName = "Grades"
    let id = Column("id", Int32.self, primaryKey: true)
    let course = Column("course", String.self)
    let grade = Column("grade", Int32.self)
}

我们将添加第二个表,名为 courses

class Courses: Table {
    let tableName = "Courses"
    let name = Column("name", String.self, primaryKey: true)
    let credit = Column("credit", Int32.self)
    let teacher = Column("teacher", String.self)
}
let courses = Courses()

我们可以向 Grades 添加一个外键,该外键引用另一个表中的列

let grades = Grades().foreignKey(grades.course, references: courses.name)

创建一个多列主键(如果未在列中设置,如 Grades.id 的情况)

grades.primaryKey(grades.id, grades.course)

在数据库中创建表

courses.create(connection: connection) { result in
     guard result.success else {
        print("Failed to create table: \(result.asError?)")
     }
    ...
}

索引

您可以使用 Swift-Kuery 通过以下方式管理索引

let index = Index("index", on: grades, columns: [grades.id, desc(grades.grade)])
index.create(connection: connection) { result in ... }
...
index.drop(connection: connection) { result in ... }

迁移

Swift-Kuery 有一个 Migration 类,用于帮助在表的两个版本之间进行迁移。

假设我们的应用程序中有一个表 MyTable。建议的用法是将表类的版本保存在应用程序代码的某个位置

public class MyTable_v0: Table {
    let a = Column("a", ...)
    let b = Column("b", ...)
    let tableName = "MyTable"
}

public class MyTable_v1: Table {
    let b = Column("b", ...)
    let c = Column("c", ...)
    let tableName = "MyTable"
}

使用类型别名来引用应用程序中当前版本的表类

typealias MyTable = MyTable_v0
let t = MyTable()
let q = Select(from t)
...

从 v0 到 v1 的迁移代码应如下所示

let t0 = MyTable_v0()
let t1 = MyTable_v1()
let migration0 = Migration(from: t0, to: t1, using: connection)
migration0.alterTableAdd(column: t1.c) { result in ... }

如果需要,您还可以执行原始的 alter 操作

let dropColumnQuery = "ALTER TABLE " + t1.tableName + " DROP COLUMN " + t0.a.name
connection.execute(dropColumnQuery) { result in ... }

查询示例

在以下部分中,我们将提供一个 SQL 查询示例,并向您展示如何使用 Swift-Kuery 在 Swift 中构建和执行相同的查询。

示例中使用的类

在这些示例中,我们将使用以下两个表

class T1 {
  let tableName = "t1"
  let a = Column("a")
  let b = Column("b")
}

class T2 {
  let tableName = "t2"
  let c = Column("c")
  let b = Column("b")
}

 

SELECT * FROM t1;

此查询将从表中选择所有结果。下面的示例展示了如何执行此查询,包括样板代码

let t1 = T1()

let query = Select(from: t1)

pool.getConnection() { connection, error in
    guard let connection = connection else {
        // Handle error
        return
    }
    query.execute(connection) { queryResult in
        guard let resultSet = queryResult.asResultSet else {
            // Handle error
            return
        }
        resultSet.getColumnTitles() { titles, error in
            guard let titles = titles else {
                // Handle error
                return
            }
            //Process titles
            resultSet.forEach() { row, error in
                guard let row = row else {
                    // Processed all results
                    return
                }
                // Process row
            }
        }
    }
}

以下示例展示了更复杂的查询,可以将其替换到上面的样板代码中。  

SELECT a, b FROM t1
WHERE (a LIKE '%b' OR a = 'apple') AND b > 5
ORDER BY b ASC, a DESC
OFFSET 5;

let query = Select(t1.a, t1.b, from: t1)
  .where((t1.a.like("b%") || t1.a == "apple") && t1.b > 5)
  .order(by: .ASC(t1.b), .DESC(t1.a))
  .offset(5)

 

SELECT UCASE(a) AS name FROM t1
WHERE b >= 0
GROUP BY a
HAVING SUM(b) > 3
ORDER BY a DESC;

let query = Select(ucase(t1.a).as("name"), from: t1)
  .where(t1.b >= 0)
  .group(by: t1.a)
  .having(sum(t1.b) > 3)
  .order(by: .DESC(t1.a))

 

INSERT INTO t1
VALUES ('apple', 10), ('apricot', 3), ('banana', 17);

let query = Insert(into: t1, rows: [["apple", 10], ["apricot", 3], ["banana", 17]])

 

INSERT INTO t1
VALUES ('apple', 10);

let query = Insert(into: t1, values: "apple", 10)

 

INSERT INTO t1 (a, b)
VALUES ('apricot', '3');

let query = Insert(into: t1, valueTuples: (t1.a, "apricot"), (t1.b, "3"))

 

INSERT INTO t1 (a, b)
VALUES ('apricot', '3');

let query = Insert(into: t1, columns: [t1.a, t1.b], values: ["apricot", 3])

 

UPDATE t1 SET a = 'peach', b = 2
WHERE a = 'banana';

let query = Update(t1, set: [(t1.a, "peach"), (t1.b, 2)])
  .where(t1.a == "banana")

 

SELECT * FROM t1 AS left
LEFT JOIN t2 AS right
ON left.b = right.b;

let t1 = T1()
let t2 = T2()

let leftTable = t1.as("left")
let rightTable = t2.as("right")
let query = Select(from: leftTable)
  .leftJoin(rightTable)
  .on(leftTable.b == rightTable.b)

 

SELECT * FROM t1
JOIN t2
USING (b);

let query = Select(from: t1)
  .join(t2)
  .using(t1.b)

 

带参数的查询

注意:命名参数受所有数据库支持,即使对于那些不支持命名参数的数据库(例如 PostgreSQL)。

INSERT INTO t1
VALUES (@0,@1);

let query = Insert(into: t1, values: Parameter(), Parameter())

connection.execute(query: query, parameters: "banana", 28) { queryResult in
  // Process result
}

 

INSERT INTO t1
VALUES (@fruit,@number);

let query = Insert(into: t1, values: Parameter("fruit"), Parameter("number"))

connection.execute(query: query, parameters: ["number" : 28, "fruit" : "banana"]) { queryResult in
  // Process result
}

 

可以使用参数插入 NULL 值:  

connection.execute(query: query, parameters: ["number" : 28, "fruit" : nil]) { queryResult in
  // Process result
}

 

原始查询

connection.execute("CREATE TABLE myTable (a varchar(40), b integer)") {  queryResult in
  // Process result
}

 

SELECT LEFT(a, 2) as raw FROM t1
WHERE b >= 0 GROUP BY a
HAVING sum(b) > 3
ORDER BY a DESC;

let query = Select(RawField("LEFT(a, 2) as raw"), from: t1)
  .where("b >= 0")
  .group(by: t1.a)
  .having("sum(b) > 3")
  .order(by: .DESC(t1.a))

 

SELECT * FROM t1
WHERE b >= ANY (SELECT b FROM t2);

let query = Select(from: t1)
  .where(t1.b >= any(Select(t2.b, from: t2)))

 

SELECT * FROM t1
WHERE NOT EXISTS (SELECT * FROM t2 WHERE b < 8);

let query = Select(from: t1)
  .where(notExists(Select(from: t2).where(t2.b < 8)))

 

SELECT c FROM t2 GROUP BY c
HAVING SUM(b) NOT IN (SELECT b FROM t1 WHERE a = 'apple');

let query = Select(t2.c, from: t2)
    .group(by: t2.c)
    .having(sum(t2.b).notIn(Select(t1.b, from: t1).where(t1.a == "apple")))

插件列表

API 文档

有关更多信息,请访问我们的 API 参考

社区

我们很乐意讨论服务器端 Swift 和 Kitura。加入我们的 Slack 与团队会面!

许可证

此库在 Apache 2.0 许可下获得许可。完整许可文本可在 LICENSE 中找到。