完美的 CRUD 简体中文

CRUD 是一个用于 Swift 4+ 的对象关系映射 (ORM) 系统。CRUD 接受 Swift 4 的 Codable 类型,并将它们映射到 SQL 数据库表。CRUD 可以基于 Codable 类型创建表,并在这些表中执行对象的插入和更新操作。CRUD 还可以执行表的选择和连接操作,所有操作均以类型安全的方式进行。

CRUD 使用一种简单、富有表现力且类型安全的方法来构建查询,该方法将查询构建为一系列操作。它被设计为轻量级的,并且没有额外的依赖项。它使用泛型、KeyPath 和 Codable,以确保尽可能在编译时捕获错误使用。

数据库客户端库包可以通过实现一些协议来添加 CRUD 支持。目前支持 SQLitePostgresMySQL

要在您的项目中使用 CRUD,只需将您选择的数据库连接器作为依赖项包含在您的 Package.swift 文件中。例如

// postgres
.package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", from: "3.2.0")
// mysql
.package(url: "https://github.com/PerfectlySoft/Perfect-MySQL.git", from: "3.2.0")
// sqlite
.package(url: "https://github.com/PerfectlySoft/Perfect-SQLite.git", from: "3.1.0")

CRUD 支持直接构建到这些数据库连接器包中。

目录

通用用法

这是一个简单的示例,展示如何使用 CRUD。

// CRUD can work with most Codable types.
struct PhoneNumber: Codable {
	let personId: UUID
	let planetCode: Int
	let number: String
}
struct Person: Codable {
	let id: UUID
	let firstName: String
	let lastName: String
	let phoneNumbers: [PhoneNumber]?
}

// CRUD usage begins by creating a database connection.
// The inputs for connecting to a database will differ depending on your client library.
// Create a `Database` object by providing a configuration.
// These examples will use SQLite for demonstration purposes,
// 	but all code would be identical regardless of the datasource type.
let db = Database(configuration: try SQLiteDatabaseConfiguration(testDBName))

// Create the table if it hasn't been done already.
// Table creates are recursive by default, so "PhoneNumber" is also created here.
try db.create(Person.self, policy: .reconcileTable)

// Get a reference to the tables we will be inserting data into.
let personTable = db.table(Person.self)
let numbersTable = db.table(PhoneNumber.self)

// Add an index for personId, if it does not already exist.
try numbersTable.index(\.personId)

// Insert some sample data.
do {
	// Insert some sample data.
	let owen = Person(id: UUID(), firstName: "Owen", lastName: "Lars", phoneNumbers: nil)
	let beru = Person(id: UUID(), firstName: "Beru", lastName: "Lars", phoneNumbers: nil)
	
	// Insert the people
	try personTable.insert([owen, beru])
	
	// Give them some phone numbers
	try numbersTable.insert([
		PhoneNumber(personId: owen.id, planetCode: 12, number: "555-555-1212"),
		PhoneNumber(personId: owen.id, planetCode: 15, number: "555-555-2222"),
		PhoneNumber(personId: beru.id, planetCode: 12, number: "555-555-1212")])
}

// Perform a query.
// Let's find all people with the last name of Lars which have a phone number on planet 12.
let query = try personTable
		.order(by: \.lastName, \.firstName)
	.join(\.phoneNumbers, on: \.id, equals: \.personId)
		.order(descending: \.planetCode)
	.where(\Person.lastName == "Lars" && \PhoneNumber.planetCode == 12)
	.select()

// Loop through the results and print the names.
for user in query {
	// We joined PhoneNumbers, so we should have values here.
	guard let numbers = user.phoneNumbers else {
		continue
	}
	for number in numbers {
		print(number.number)
	}
}

操作

CRUD 中的活动通过获取数据库连接对象,然后在该数据库上链式调用一系列操作来完成。某些操作立即执行,而其他操作(例如 select)则延迟执行。每个链式操作都会返回一个对象,该对象可以进一步链式调用或执行。

操作在此处根据实现它们的对象进行分组。请注意,下面显示的许多类型定义为了简单起见已被缩写,并且在扩展中实现的一些函数已被移动到一个代码块中以保持内容集中。

数据库

Database 对象封装并维护与数据库的连接。数据库连接通过使用 DatabaseConfigurationProtocol 对象来指定。这些对象将特定于所使用的数据库。

// postgres sample configuration
let db = Database(configuration: 
	try PostgresDatabaseConfiguration(database: postgresTestDBName, host: "localhost"))
// sqlite sample configuration
let db = Database(configuration: 
	try SQLiteDatabaseConfiguration(testDBName))
// MySQL sample configuration
let db = Database(configuration:
    	try MySQLDatabaseConfiguration(database: "testDBName", host: "localhost", username: "username", password: "password"))

Database 对象实现以下逻辑函数集

public struct Database<C: DatabaseConfigurationProtocol>: DatabaseProtocol {
	public typealias Configuration = C
	public let configuration: Configuration
	public init(configuration c: Configuration)
	public func table<T: Codable>(_ form: T.Type) -> Table<T, Database<C>>
	public func transaction<T>(_ body: () throws -> T) throws -> T
	public func create<A: Codable>(_ type: A.Type, 
		primaryKey: PartialKeyPath<A>? = nil, 
		policy: TableCreatePolicy = .defaultPolicy) throws -> Create<A, Self>
}

Database 对象上可用的操作包括 transactioncreatetable

事务

transaction 操作将在 "BEGIN" 和 "COMMIT" 或 "ROLLBACK" 语句之间执行主体代码。如果主体代码执行完成且未抛出错误,则事务将被提交,否则将被回滚。

public extension Database {
	func transaction<T>(_ body: () throws -> T) throws -> T
}

用法示例

try db.transaction {
	... further operations
}

事务的主体代码可以选择性地返回值。

let value = try db.transaction {
	... further operations
	return 42
}

创建

create 操作接受一个 Codable 类型。它将创建一个与该类型的结构相对应的表。可以指定表的主键以及“创建策略”,该策略确定操作的某些方面。

public extension DatabaseProtocol {
	func create<A: Codable>(
		_ type: A.Type, 
		primaryKey: PartialKeyPath<A>? = nil, 
		policy: TableCreatePolicy = .defaultPolicy) throws -> Create<A, Self>
}

用法示例

try db.create(TestTable1.self, primaryKey: \.id, policy: .reconcileTable)

TableCreatePolicy 包含以下选项

对已存在的表调用 create 是一个无害的操作,除非指示了 .reconcileTable.dropTable 策略,否则不会导致任何更改。除非指示了 .reconcileTable,否则现有表不会被修改以匹配相应 Codable 类型中的更改。

table 操作基于指示的 Codable 类型返回一个 Table 对象。Table 对象用于执行进一步的操作。

public protocol DatabaseProtocol {
	func table<T: Codable>(_ form: T.Type) -> Table<T, Self>
}

用法示例

let table1 = db.table(TestTable1.self)

SQL

CRUD 还可以执行定制的 SQL 语句,并将结果映射到任何合适的 Codable 类型的数组。

public extension Database {
	func sql(_ sql: String, bindings: Bindings = []) throws
	func sql<A: Codable>(_ sql: String, bindings: Bindings = [], _ type: A.Type) throws -> [A]
}

用法示例

try db.sql("SELECT * FROM mytable WHERE id = 2", TestTable1.self)

Table 可以跟随:Database

Table 支持:updateinsertdeletejoinorderlimitwhereselectcount

Table 对象可用于执行更新、插入、删除或选择操作。表只能通过数据库对象访问,方法是提供要映射的 Codable 类型。表对象在一个操作链中只能出现一次,并且必须是第一个项目。

Table 对象基于您在检索表时提供的 Swift 对象类型进行参数化。表指示任何操作的总体结果类型。这将被称为 OverAllForm

用法示例

// get a table object representing the TestTable1 struct
// any inserts, updates, or deletes will affect "TestTable1"
// any selects will produce a collection of TestTable1 objects.
let table1 = db.table(TestTable1.self)

在上面的示例中,TestTable1 是 OverAllForm。任何破坏性操作都会影响相应的数据库表。任何选择操作都将生成 TestTable1 对象的集合。

索引

Index 可以跟随:table

数据库索引对于良好的查询性能至关重要。给定一个表对象,可以通过调用 index 函数添加数据库索引。索引应与创建表的代码一起添加。

index 函数接受一个或多个表键路径。

用法示例

struct Person: Codable {
    let id: UUID
    let firstName: String
    let lastName: String
}
// create the Person table
try db.create(Person.self)
// get a table object representing the Person struct
let table = db.table(Person.self)
// add index for lastName column
try table.index(\.lastName)
// add unique index for firstName & lastName columns
try table.index(unique: true, \.firstName, \.lastName)

可以为单个列或作为一组列创建索引。如果多个列经常在查询中一起使用,那么通过添加包含这些列的索引通常可以提高性能。

通过包含 unique: true 参数,将创建一个唯一索引,这意味着只有一行可以包含任何可能的列值。这可以应用于多个列,如上面的示例所示。有关数据库索引的确切行为,请查阅您特定数据库的文档。

index 函数定义为

public extension Table {
	func index(unique: Bool = false, _ keys: PartialKeyPath<OverAllForm>...) throws -> Index<OverAllForm, Table>
}

连接

Join 可以跟随:tableorderlimit 或另一个 join

Join 支持:joinwhereorderlimitselectcount

join 引入了来自另一个表的对象,可以是父子关系或多对多关系方案。

父子关系 用法示例

struct Parent: Codable {
	let id: Int
	let children: [Child]?
}
struct Child: Codable {
	let id: Int
	let parentId: Int
}
try db.transaction {
	try db.create(Parent.self, policy: [.shallow, .dropTable]).insert(
		Parent(id: 1, children: nil))
	try db.create(Child.self, policy: [.shallow, .dropTable]).insert(
		[Child(id: 1, parentId: 1),
		 Child(id: 2, parentId: 1),
		 Child(id: 3, parentId: 1)])
}
let join = try db.table(Parent.self)
	.join(\.children,
		  on: \.id,
		  equals: \.parentId)
	.where(\Parent.id == 1)
guard let parent = try join.first() else {
	return XCTFail("Failed to find parent id: 1")
}
guard let children = parent.children else {
	return XCTFail("Parent had no children")
}
XCTAssertEqual(3, children.count)
for child in children {
	XCTAssertEqual(child.parentId, parent.id)
}

上面的示例在 Parent 对象上连接了 Child 对象,连接依据是 Parent 对象的 children 属性,该属性的类型为 [Child]?。当查询执行时,Child 表中所有 parentId 与 Parent id 1 匹配的对象都将包含在结果中。这是一个典型的父子关系。

多对多关系 用法示例

struct Student: Codable {
	let id: Int
	let classes: [Class]?
}
struct Class: Codable {
	let id: Int
	let students: [Student]?
}
struct StudentClasses: Codable {
	let studentId: Int
	let classId: Int
}
try db.transaction {
	try db.create(Student.self, policy: [.dropTable, .shallow]).insert(
		Student(id: 1, classes: nil))
	try db.create(Class.self, policy: [.dropTable, .shallow]).insert([
		Class(id: 1, students: nil),
		Class(id: 2, students: nil),
		Class(id: 3, students: nil)])
	try db.create(StudentClasses.self, policy: [.dropTable, .shallow]).insert([
		StudentClasses(studentId: 1, classId: 1),
		StudentClasses(studentId: 1, classId: 2),
		StudentClasses(studentId: 1, classId: 3)])
}
let join = try db.table(Student.self)
	.join(\.classes,
		  with: StudentClasses.self,
		  on: \.id,
		  equals: \.studentId,
		  and: \.id,
		  is: \.classId)
	.where(\Student.id == 1)
guard let student = try join.first() else {
	return XCTFail("Failed to find student id: 1")
}
guard let classes = student.classes else {
	return XCTFail("Student had no classes")
}
XCTAssertEqual(3, classes.count)
for aClass in classes {
	let join = try db.table(Class.self)
		.join(\.students,
			  with: StudentClasses.self,
			  on: \.id,
			  equals: \.classId,
			  and: \.id,
			  is: \.studentId)
		.where(\Class.id == aClass.id)
	guard let found = try join.first() else {
		XCTFail("Class with no students")
		continue
	}
	guard nil != found.students?.first(where: { $0.id == student.id }) else {
		XCTFail("Student not found in class")
		continue
	}
}

自连接 用法示例

struct Me: Codable {
	let id: Int
	let parentId: Int
	let mes: [Me]?
	init(id i: Int, parentId p: Int) {
		id = i
		parentId = p
		mes = nil
	}
}
try db.transaction {
	try db.create(Me.self, policy: .dropTable).insert([
		Me(id: 1, parentId: 0),
		Me(id: 2, parentId: 1),
		Me(id: 3, parentId: 1),
		Me(id: 4, parentId: 1),
		Me(id: 5, parentId: 1)])
}
let join = try db.table(Me.self)
	.join(\.mes, on: \.id, equals: \.parentId)
	.where(\Me.id == 1)
guard let me = try join.first() else {
	return XCTFail("Unable to find me.")
}
guard let mes = me.mes else {
	return XCTFail("Unable to find meesa.")
}
XCTAssertEqual(mes.count, 4)

连接表连接 用法示例

struct Student: Codable {
	let id: Int
	let classes: [Class]?
	init(id i: Int) {
		id = i
		classes = nil
	}
}
struct Class: Codable {
	let id: Int
	let students: [Student]?
	init(id i: Int) {
		id = i
		students = nil
	}
}
struct StudentClasses: Codable {
	let studentId: Int
	let classId: Int
}
try db.transaction {
	try db.create(Student.self, policy: [.dropTable, .shallow]).insert(
		Student(id: 1))
	try db.create(Class.self, policy: [.dropTable, .shallow]).insert([
		Class(id: 1),
		Class(id: 2),
		Class(id: 3)])
	try db.create(StudentClasses.self, policy: [.dropTable, .shallow]).insert([
		StudentClasses(studentId: 1, classId: 1),
		StudentClasses(studentId: 1, classId: 2),
		StudentClasses(studentId: 1, classId: 3)])
}
let join = try db.table(Student.self)
	.join(\.classes,
		  with: StudentClasses.self,
		  on: \.id,
		  equals: \.studentId,
		  and: \.id,
		  is: \.classId)
	.where(\Student.id == 1)
guard let student = try join.first() else {
	return XCTFail("Failed to find student id: 1")
}
guard let classes = student.classes else {
	return XCTFail("Student had no classes")
}
XCTAssertEqual(3, classes.count)

更新、插入或删除操作目前不支持连接(不支持级联删除/递归更新)。

Join 协议有两个函数。第一个处理标准的双表连接。第二个处理连接表(三表)连接。

public protocol JoinAble: TableProtocol {
	// standard join
	func join<NewType: Codable, KeyType: Equatable>(
		_ to: KeyPath<OverAllForm, [NewType]?>,
		on: KeyPath<OverAllForm, KeyType>,
		equals: KeyPath<NewType, KeyType>) throws -> Join<OverAllForm, Self, NewType, KeyType>
	// junction join
	func join<NewType: Codable, Pivot: Codable, FirstKeyType: Equatable, SecondKeyType: Equatable>(
		_ to: KeyPath<OverAllForm, [NewType]?>,
		with: Pivot.Type,
		on: KeyPath<OverAllForm, FirstKeyType>,
		equals: KeyPath<Pivot, FirstKeyType>,
		and: KeyPath<NewType, SecondKeyType>,
		is: KeyPath<Pivot, SecondKeyType>) throws -> JoinPivot<OverAllForm, Self, NewType, Pivot, FirstKeyType, SecondKeyType>
}

标准连接需要三个参数

to - 指向 OverAllForm 属性的键路径。此键路径应指向非整型 Codable 类型的可选数组。此属性将使用结果对象进行设置。

on - 指向 OverAllForm 属性的键路径,该属性应被用作连接的主键(通常会使用实际的表主键列)。

equals - 指向连接类型的属性的键路径,该属性应等于 OverAllForm 的 on 属性。这将是外键。

连接表连接需要六个参数

to - 指向 OverAllForm 属性的键路径。此键路径应指向非整型 Codable 类型的可选数组。此属性将使用结果对象进行设置。

with - 连接表的类型。

on - 指向 OverAllForm 属性的键路径,该属性应被用作连接的主键(通常会使用实际的表主键列)。

equals - 指向连接类型的属性的键路径,该属性应等于 OverAllForm 的 on 属性。这将是外键。

and - 指向子表类型的属性的键路径,该属性应被用作连接的键(通常会使用实际的表主键列)。

is - 指向连接类型的属性的键路径,该属性应等于子表的 and 属性。

任何未显式包含在连接中的连接类型表,对于任何生成的 OverAllForm 对象都将设置为 nil。

如果连接表包含在连接中,但没有生成任何连接对象,则 OverAllForm 的属性将设置为空数组。

Where 条件

Where 可以跟随:tablejoinorder

Where 支持:selectcountupdate(当跟随 table 时)、delete(当跟随 table 时)。

where 操作引入了一个条件,该条件将用于过滤应从数据库中选择、更新或删除的确切对象。Where 条件只能在执行 select/count、update 或 delete 时使用。

public protocol WhereAble: TableProtocol {
	func `where`(_ expr: CRUDBooleanExpression) -> Where<OverAllForm, Self>
}

Where 操作是可选的,但一个操作链中只能包含一个 where,并且它必须是链中的倒数第二个操作。

用法示例

let table = db.table(TestTable1.self)
// insert a new object and then find it
let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
try table.insert(newOne)
// search for this one object by id
let query = table.where(\TestTable1.id == newOne.id)
guard let foundNewOne = try query.first() else {
	...
}

提供给 where 操作的参数是 CRUDBooleanExpression 对象。这些对象通过使用任何受支持的表达式运算符生成。

标准 Swift 运算符

• 相等性:==!=

• 比较:<<=>>=

• 逻辑:!&&||

自定义比较运算符

• 包含/在其中:~!~

• Like:%=%=%%=%!=%!=%%!=

对于相等性和比较运算符,左侧操作数必须是 KeyPath,指示 Codable 类型的 Codable 属性。右侧操作数可以是 Int、Double、String、[UInt8]、Bool、UUID 或 Date。KeyPath 可以指示可选属性值,在这种情况下,右侧操作数可以是 nil 以指示 “IS NULL”、“IS NOT NULL” 类型的查询。

相等性和比较运算符是类型安全的,这意味着您不能在例如 Int 和 String 之间进行比较。右侧操作数的类型必须与 KeyPath 属性类型匹配。这是 Swift 的正常工作方式,因此不应带来任何意外。

通过 tablejoin 操作引入到查询中的任何类型都可以在表达式中使用。对查询中其他地方未使用的类型使用 KeyPath 是运行时错误。

在此代码片段中

table.where(\TestTable1.id > 20)

\TestTable1.id 是 KeyPath,指向对象的 Int id。20 是字面量操作数值。它们之间的 > 运算符生成一个 CRUDBooleanExpression,可以直接提供给 where 或与其他运算符一起使用以创建更复杂的表达式。

逻辑运算符允许对两个 CRUDBooleanExpression 对象进行 andornot 运算。这些运算符使用标准的 Swift &&||! 运算符。

table.where(\TestTable1.id > 20 && 
	!(\TestTable1.name == "Me" || \TestTable1.name == "You"))

包含/在其中运算符在右侧接受 KeyPath,在左侧接受对象数组。

table.where(\TestTable1.id ~ [2, 4])
table.where(\TestTable1.id !~ [2, 4])

以上代码将选择所有 id 在数组中或不在数组中的 TestTable1 对象。

Like 运算符仅与 String 值一起使用。这些运算符允许对基于 String 的列进行以…开头、以…结尾和包含搜索。

try table.where(\TestTable2.name %=% "me") // contains
try table.where(\TestTable2.name =% "me") // begins with
try table.where(\TestTable2.name %= "me") // ends with
try table.where(\TestTable2.name %!=% "me") // not contains
try table.where(\TestTable2.name !=% "me") // not begins with
try table.where(\TestTable2.name %!= "me") // not ends with

属性可选值

在某些情况下,您可能需要在使用模型中的可选参数进行查询。在上面的 Person 模型中,我们可能需要添加一个可选的 height 字段

struct Person: Codable {
	// ...
	var height: Double? // height in cm
}

如果我们想搜索尚未提供身高的 People,这个简单的查询将找到所有 heightNULL 的行。

let people = try personTable.where(\Person.height == nil).select().map{ $0 }

或者,您可能需要查询身高达到或超过某个高度的个人。

let queryHeight = 170.0
let people = try personTable.where(\Person.height! >= queryHeight).select().map{ $0 }

请注意强制解包的键路径 - \Person.height!这是类型安全的,并且编译器需要这样做,以便将模型上的可选类型与查询中的非可选值进行比较。

Order 排序

Order 可以跟随:tablejoin

Order 支持:joinwhereorderlimit selectcount

order 操作引入了对总体结果对象和/或为特定连接选择的对象的排序。order 操作应紧跟在 tablejoin 之后。您还可以对具有可选类型的字段进行排序。

public protocol OrderAble: TableProtocol {
	func order(by: PartialKeyPath<Form>...) -> Ordering<OverAllForm, Self>
	func order(descending by: PartialKeyPath<Form>...) -> Ordering<OverAllForm, Self>
}

用法示例

let query = try db.table(TestTable1.self)
				.order(by: \.name)
			.join(\.subTables, on: \.id, equals: \.parentId)
				.order(by: \.id)
			.where(\TestTable2.name == "Me")

当执行上述查询时,它将对返回对象的主列表及其各个 “subTables” 集合应用排序。

按可为空字段排序

struct Person: Codable {
	// ...
	var height: Double? // height in cm
}

// ...

let person = try personTable.order(descending: \.height).select().map {$0}

Limit 限制

Limit 可以跟随:orderjointable

Limit 支持:joinwhereorderselectcount

limit 操作可以跟随 tablejoinorder 操作。Limit 既可以对结果对象的数量应用上限,也可以强制跳过值。例如,可以跳过前五个找到的记录,结果集将从第六行开始。

public protocol LimitAble: TableProtocol {
	func limit(_ max: Int, skip: Int) -> Limit<OverAllForm, Self>
}

Int 的范围也可以传递给 limit 函数。这包括 a..<ba...ba......b..<b 形式的范围。

public extension Limitable {
	func limit(_ range: Range<Int>) -> Limit<OverAllForm, Self>
	func limit(_ range: ClosedRange<Int>) -> Limit<OverAllForm, Self>
	func limit(_ range: PartialRangeFrom<Int>) -> Limit<OverAllForm, Self>
	func limit(_ range: PartialRangeThrough<Int>) -> Limit<OverAllForm, Self>
	func limit(_ range: PartialRangeUpTo<Int>) -> Limit<OverAllForm, Self>
}

limit 仅应用于最近的 tablejoin。放置在 table 之后的 limit 限制结果的总数。放置在 join 之后的 limit 限制返回的连接类型对象的数量。

用法示例

let query = try db.table(TestTable1.self)
				.order(by: \.name)
				.limit(20..<30)
			.join(\.subTables, on: \.id, equals: \.parentId)
				.order(by: \.id)
				.limit(1000)
			.where(\TestTable2.name == "Me")

更新

Update 可以跟随:tablewhere(当 where 跟随 table 时)。

Update 支持:立即执行。

update 操作可用于替换与查询匹配的现有记录中的值。update 几乎总是会在链中包含 where 操作,但这不是必需的。在链中不提供 where 操作将匹配所有记录。

public protocol UpdateAble: TableProtocol {
	func update(_ instance: OverAllForm, setKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Update<OverAllForm, Self>
	func update(_ instance: OverAllForm, ignoreKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Update<OverAllForm, Self>
	func update(_ instance: OverAllForm) throws -> Update<OverAllForm, Self>
}

update 需要 OverAllForm 的实例。此实例提供将在与查询匹配的任何记录中设置的值。可以使用 setKeysignoreKeys 参数执行更新,或者不使用其他参数来指示应将所有列都包含在更新中。

用法示例

let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
let newId: Int = try db.transaction {
	try db.table(TestTable1.self).insert(newOne)
	let newOne2 = TestTable1(id: 2000, name: "New One Updated", integer: 41)
	try db.table(TestTable1.self)
		.where(\TestTable1.id == newOne.id)
		.update(newOne2, setKeys: \.name)
	return newOne2.id
}
let j2 = try db.table(TestTable1.self)
	.where(\TestTable1.id == newId)
	.select().map { $0 }
XCTAssertEqual(1, j2.count)
XCTAssertEqual(2000, j2[0].id)
XCTAssertEqual("New One Updated", j2[0].name)
XCTAssertEqual(40, j2[0].integer)

插入

Insert 可以跟随:table

Insert 支持:立即执行。

Insert 用于向数据库添加新记录。可以一次插入一个或多个对象。可以添加或排除特定的键/列。insert 必须紧跟在 table 之后。

public extension Table {
	func insert(_ instances: [Form]) throws -> Insert<Form, Table<A,C>>
	func insert(_ instance: Form) throws -> Insert<Form, Table<A,C>>
	func insert(_ instances: [Form], setKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Insert<Form, Table<A,C>>
	func insert(_ instance: Form, setKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Insert<Form, Table<A,C>>
	func insert(_ instances: [Form], ignoreKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Insert<Form, Table<A,C>>
	func insert(_ instance: Form, ignoreKeys: PartialKeyPath<OverAllForm>, _ rest: PartialKeyPath<OverAllForm>...) throws -> Insert<Form, Table<A,C>>
}

用法示例

let table = db.table(TestTable1.self)
let newOne = TestTable1(id: 2000, name: "New One", integer: 40, double: nil, blob: nil, subTables: nil)
let newTwo = TestTable1(id: 2001, name: "New One", integer: 40, double: nil, blob: nil, subTables: nil)
try table.insert([newOne, newTwo], setKeys: \.id, \.name)

删除

Delete 可以跟随:tablewhere(当 where 跟随 table 时)。

Delete 支持:立即执行。

delete 操作用于从表中删除与查询匹配的记录。delete 几乎总是会在链中包含 where 操作,但这不是必需的。在链中不提供 where 操作将删除所有记录。

public protocol DeleteAble: TableProtocol {
	func delete() throws -> Delete<OverAllForm, Self>
}

用法示例

let table = db.table(TestTable1.self)
let newOne = TestTable1(id: 2000, name: "New One", integer: 40, double: nil, blob: nil, subTables: nil)
try table.insert(newOne)
let query = table.where(\TestTable1.id == newOne.id)
let j1 = try query.select().map { $0 }
assert(j1.count == 1)
try query.delete()
let j2 = try query.select().map { $0 }
assert(j2.count == 0)

Select & Count

Select 可以跟随:whereorderlimitjointable

Select 支持:迭代。

Select 返回一个可用于迭代结果值的对象。

public protocol SelectAble: TableProtocol {
	func select() throws -> Select<OverAllForm, Self>
	func count() throws -> Int
	func first() throws -> OverAllForm?
}

Count 的工作方式与 select 类似,但它将立即执行查询并仅返回结果对象的数量。实际上不获取对象数据。

用法示例

let table = db.table(TestTable1.self)
let query = table.where(\TestTable1.blob == nil)
let values = try query.select().map { $0 }
let count = try query.count()
assert(count == values.count)

数据库特定操作

MySQL

最后插入 ID

lastInsertId() 可以跟随:insert

可以在插入后调用 lastInsertId() 函数。如果可用,它将返回最后插入的 id。

public extension Insert {
	func lastInsertId() throws -> UInt64?
}

用法示例

let id = try table
	.insert(ReturningItem(id: 0, def: 0),
			ignoreKeys: \ReturningItem.id)
	.lastInsertId()

SQLite

最后插入 ID

lastInsertId() 可以跟随:insert

可以在插入后调用 lastInsertId() 函数。如果可用,它将返回最后插入的 id。

public extension Insert {
	func lastInsertId() throws -> Int?
}

PostgreSQL

返回

Returning 可以跟随:wheretable

Returning 执行插入或更新操作,并从插入/更新的行中返回值。Returning 可以返回列值或表示当前表的 Codable 对象。

插入

public extension Table where C.Configuration == PostgresDatabaseConfiguration {
	func returning<R: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: Form) throws -> R
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: Form,
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> R
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: Form,
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> R
	func returning<R: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: [Form]) throws -> [R]
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: [Form],
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [R]
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			insert: [Form],
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [R]
}

public extension Table where C.Configuration == PostgresDatabaseConfiguration {
	func returning(
			insert: Form) throws -> OverAllForm
	func returning<Z: Decodable>(
			insert: Form,
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> OverAllForm
	func returning<Z: Decodable>(
			insert: Form,
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> OverAllForm
	func returning(
			insert: [Form]) throws -> [OverAllForm]
	func returning<Z: Decodable>(
			insert: [Form],
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [OverAllForm]
	func returning<Z: Decodable>(
			insert: [Form],
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [OverAllForm]
}

更新

public extension Table where C.Configuration == PostgresDatabaseConfiguration {
	func returning<R: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			update: Form) throws -> [R]
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			update: Form,
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [R]
	func returning<R: Decodable, Z: Decodable>(
			_ returning: KeyPath<OverAllForm, R>, 
			update: Form,
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [R]
	func returning(
			update: Form) throws -> [OverAllForm]
	func returning<Z: Decodable>(
			update: Form,
			setKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [OverAllForm]
	func returning<Z: Decodable>(
			update: Form,
			ignoreKeys: KeyPath<OverAllForm, Z>, 
			_ rest: PartialKeyPath<OverAllForm>...) throws -> [OverAllForm]
}

用法示例

struct ReturningItem: Codable, Equatable {
	let id: UUID
	let def: Int?
	init(id: UUID, def: Int? = nil) {
		self.id = id
		self.def = def
	}
}
try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id UUID PRIMARY KEY, def int DEFAULT 42)")
let table = db.table(ReturningItem.self)
// returning inserts
do {
	let item = ReturningItem(id: UUID())
	let def = try table.returning(\.def, insert: item, ignoreKeys: \.def)
	XCTAssertEqual(def, 42)
}
do {
	let items = [ReturningItem(id: UUID()),
				 ReturningItem(id: UUID()),
				 ReturningItem(id: UUID())]
	let defs = try table.returning(\.def, insert: items, ignoreKeys: \.def)
	XCTAssertEqual(defs, [42, 42, 42])
}
do {
	let id = UUID()
	let item = ReturningItem(id: id, def: 42)
	let id0 = try table.returning(\.id, insert: item)
	XCTAssertEqual(id0, id)
}
do {
	let items = [ReturningItem(id: UUID()),
				 ReturningItem(id: UUID()),
				 ReturningItem(id: UUID())]
	let defs = try table.returning(insert: items, ignoreKeys: \.def)
	XCTAssertEqual(defs.map{$0.id}, items.map{$0.id})
	XCTAssertEqual(defs.compactMap{$0.def}.count, defs.count)
}

// returning update
do {
	let id = UUID()
	var item = ReturningItem(id: id)
	try table.insert(item, ignoreKeys: \.def)
	item.def = 300
	let item0 = try table
		.where(\ReturningItem.id == id)
		.returning(\.def, update: item, ignoreKeys: \.id)
	XCTAssertEqual(item0.count, 1)
	XCTAssertEqual(item.def, item0.first)
}

Codable 类型

大多数 Codable 类型都可以与 CRUD 一起使用,通常情况下,根据您的需要,无需进行修改。类型的所有相关属性都将映射到数据库表中的列。您可以通过向类型添加 CodingKeys 属性来自定义列名。

默认情况下,类型名称将用作表名。要自定义类型表使用的名称,请使该类型实现 TableNameProvider 协议。这需要一个 static let tableName: String 属性。

CRUD 支持以下属性类型

这些类型在数据库中的实际存储将取决于所使用的客户端库。例如,Postgres 将具有实际的 “date” 和 “uuid” 列类型,而在 SQLite 中,这些类型将存储为字符串。

与 CRUD 一起使用的类型还可以具有一个或多个子类型或连接类型的数组。这些数组可以使用查询中的 join 操作填充。请注意,不会为连接类型属性创建表列。

以下示例类型说明了使用 CodingKeysTableNameProvider 和连接类型的有效 CRUD Codables

struct TestTable1: Codable, TableNameProvider {
	enum CodingKeys: String, CodingKey {
		// specify custom column names for some properties
		case id, name, integer = "int", double = "doub", blob, subTables
	}
	// specify a custom table name
	static let tableName = "test_table_1"
	
	let id: Int
	let name: String?
	let integer: Int?
	let double: Double?
	let blob: [UInt8]?
	let subTables: [TestTable2]?
}

struct TestTable2: Codable {
	let id: UUID
	let parentId: Int
	let date: Date
	let name: String?
	let int: Int?
	let doub: Double?
	let blob: [UInt8]?
}

连接类型应为 Codable 对象的可选数组。在上面,TestTable1 结构在其 subTables 属性上具有连接类型:let subTables: [TestTable2]?。仅当使用 join 操作连接相应的表时,才会填充连接类型。

标识

当 CRUD 创建与类型对应的表时,它会尝试确定表的主键将是什么。您可以在调用 create 操作时显式指示哪个属性是主键。如果您未指示键,则将查找名为 “id” 的属性。如果没有 “id” 属性,则将创建没有主键的表。请注意,在“浅层”创建表时可以指定自定义主键名称,但在递归创建表时不能。有关更多详细信息,请参阅“创建”操作。

错误处理

SQL 生成、执行或结果获取期间发生的任何错误都将产生抛出的 Error 对象。

对于类型编码和解码期间发生的错误,CRUD 将抛出 CRUDDecoderErrorCRUDEncoderError

对于 SQL 语句生成期间发生的错误,CRUD 将抛出 CRUDSQLGenError

对于 SQL 语句执行期间发生的错误,CRUD 将抛出 CRUDSQLExeError

所有 CRUD 错误都与日志记录系统绑定在一起。当它们被抛出时,错误消息将出现在日志中。当其他错误发生时,各个数据库客户端库可能会抛出其他错误。

日志记录

CRUD 包含一个内置的日志记录系统,旨在记录发生的错误。它还可以记录生成的单个 SQL 语句。CRUD 日志记录是异步完成的。您可以通过调用 CRUDLogging.flush() 来刷新所有挂起的日志消息。

可以通过调用 CRUDLogging.log(_ type: CRUDLogEventType, _ msg: String) 将消息添加到日志中。

用法示例

// log an informative message.
CRUDLogging.log(.info, "This is my message.")

CRUDLogEventType 是以下之一:.info.warning.error.query

您可以通过设置 CRUDLogging.queryLogDestinationsCRUDLogging.errorLogDestinations 静态属性来控制日志消息的去向。修改日志目标是线程安全的操作。错误和查询的处理可以分别设置,因为在开发期间可能需要 SQL 语句日志记录,但在生产环境中则不需要。

public extension CRUDLogging {
	public static var queryLogDestinations: [CRUDLogDestination]
	public static var errorLogDestinations: [CRUDLogDestination]
}

日志目标定义为

public enum CRUDLogDestination {
	case none
	case console
	case file(String)
	case custom((CRUDLogEvent) -> ())
}

每条消息可以发送到多个目标。默认情况下,错误和查询都记录到控制台。