CRUD 是一个用于 Swift 4+ 的对象关系映射 (ORM) 系统。CRUD 接受 Swift 4 Codable
类型,并将它们映射到 SQL 数据库表。CRUD 可以基于 Codable
类型创建表,并对这些表中的对象执行插入和更新操作。CRUD 还可以执行表的选择和连接操作,所有这些操作都以类型安全的方式进行。
CRUD 使用简单、富有表现力且类型安全的方法来构建查询,查询构建过程由一系列操作组成。它被设计为轻量级的,并且没有额外的依赖项。它使用泛型、KeyPath 和 Codable,以确保尽可能多地在编译时捕获错误用法。
数据库客户端库包可以通过实现一些协议来添加 CRUD 支持。目前支持 SQLite、Postgres 和 MySQL。
要在您的项目中使用 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 对象上可用的操作包括 transaction
、create
和 table
。
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)
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 支持:update
、insert
、delete
、join
、order
、limit
、where
、select
和 count
。
Table 对象可用于执行更新、插入、删除或选择操作。表只能通过数据库对象访问,方法是提供要映射的 Codable 类型。Table 对象在操作链中只能出现一次,并且必须是第一个项目。
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
。
数据库索引对于良好的查询性能至关重要。给定一个 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 可以跟随:table
、order
、limit
或另一个 join
。
Join 支持:join
、where
、order
、limit
、select
、count
。
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 对象的 children 属性(类型为 [Child]?
)上连接 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 可以跟随:table
、join
、order
。
Where 支持:select
、count
、update
(当跟随 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 可以指示 Optional 属性值,在这种情况下,右侧操作数可以是 nil
,以指示 “IS NULL”、“IS NOT NULL” 类型的查询。
相等性和比较运算符是类型安全的,这意味着您不能在 Int 和 String 之间进行比较。右侧操作数的类型必须与 KeyPath 属性类型匹配。这是 Swift 的正常工作方式,因此不应带来任何意外。
通过 table
或 join
操作引入到查询中的任何类型都可以在表达式中使用。为查询中其他位置未使用的类型使用 KeyPath 会导致运行时错误。
在此代码片段中
table.where(\TestTable1.id > 20)
\TestTable1.id
是 KeyPath,指向对象的 Int id。20 是字面量操作数值。它们之间的 >
运算符生成一个 CRUDBooleanExpression
,可以直接传递给 where
,或者与其他运算符一起使用以生成更复杂的表达式。
逻辑运算符允许对两个 CRUDBooleanExpression
对象进行 and
、or
和 not
操作。这些操作使用标准的 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
属性 Optional
在某些情况下,您可能需要使用模型中的可选参数进行查询。在上面的 Person
模型中,我们可能需要添加一个可选的 height
字段
struct Person: Codable {
// ...
var height: Double? // height in cm
}
如果我们想搜索尚未提供身高的 People
,这个简单的查询将找到所有 height
为 NULL
的行。
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 可以跟随:table
、join
。
Order 支持:join
、where
、order
、limit
select
、count
。
order
操作引入了总体结果对象和/或为特定连接选择的对象的排序。order 操作应紧跟 table
或 join
之后。您也可以对具有可选类型的字段进行排序。
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 可以跟随:order
、join
、table
。
Limit 支持:join
、where
、order
、select
、count
。
limit
操作可以跟随 table
、join
或 order
操作。Limit 既可以应用结果对象数量的上限,也可以施加跳过值。例如,可以跳过找到的前五个记录,结果集将从第六行开始。
public protocol LimitAble: TableProtocol {
func limit(_ max: Int, skip: Int) -> Limit<OverAllForm, Self>
}
Int 的范围也可以传递给 limit
函数。这包括 a..<b
、a...b
、a...
、...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 仅适用于最近的 table
或 join
。放置在 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 可以跟随:table
、where
(当 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 的实例。此实例提供将在与查询匹配的任何记录中设置的值。可以使用 setKeys
或 ignoreKeys
参数执行更新,或者不使用其他参数来指示应在更新中包含所有列。
使用示例
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 可以跟随:table
、where
(当 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 可以跟随:where
、order
、limit
、join
、table
。
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)
lastInsertId() 可以跟随:insert
。
lastInsertId()
函数可以在 insert 之后调用。它将返回最后插入的 id(如果可用)。
public extension Insert {
func lastInsertId() throws -> UInt64?
}
使用示例
let id = try table
.insert(ReturningItem(id: 0, def: 0),
ignoreKeys: \ReturningItem.id)
.lastInsertId()
lastInsertId() 可以跟随:insert
。
lastInsertId()
函数可以在 insert 之后调用。它将返回最后插入的 id(如果可用)。
public extension Insert {
func lastInsertId() throws -> Int?
}
Returning 可以跟随:where
、table
。
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
类型都可以与 CRUD 一起使用,通常,根据您的需要,无需修改。类型的所有相关属性都将映射到数据库表中的列。您可以通过向您的类型添加 CodingKeys
属性来自定义列名。
默认情况下,类型名称将用作表名。要自定义类型表使用的名称,请让该类型实现 TableNameProvider
协议。这需要一个 static let tableName: String
属性。
CRUD 支持以下属性类型
这些类型在数据库中的实际存储将取决于使用的客户端库。例如,Postgres 将具有实际的 “date” 和 “uuid” 列类型,而在 SQLite 中,这些类型将存储为字符串。
与 CRUD 一起使用的类型还可以具有一个或多个子类型或连接类型的数组。这些数组可以使用查询中的 join
操作填充。请注意,不会为连接类型属性创建表列。
以下示例类型说明了使用 CodingKeys
、TableNameProvider
和连接类型的有效 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” 属性,则将在不使用主键的情况下创建表。请注意,在 “浅层” 创建表时可以指定自定义主键名称,但在递归创建表时则不能。有关更多详细信息,请参阅 “Create” 操作。
SQL 生成、执行或结果获取期间发生的任何错误都将产生抛出的 Error 对象。
CRUD 将为类型编码和解码期间发生的错误抛出 CRUDDecoderError
或 CRUDEncoderError
。
CRUD 将为 SQL 语句生成期间发生的错误抛出 CRUDSQLGenError
。
CRUD 将为 SQL 语句执行期间发生的错误抛出 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.queryLogDestinations
和 CRUDLogging.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) -> ())
}
每条消息都可以发送到多个目标。默认情况下,错误和查询都记录到控制台。