尧图精选

GRDB.swift 数据库 Schema 修改实战指南:建表、改表与索引的类型安全方案

🕒 发布时间:2026/9/16 13:50:11 📁 来源:尧图网络
GRDB.swift 数据库 Schema 修改实战指南建表、改表与索引的类型安全方案【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swiftGRDB.swift 为 SQLite 数据库提供了完整的、以 Swift 类型安全方式修改数据库 Schema 的 API。本指南基于 GRDB 官方文档GRDB/Documentation.docc/DatabaseSchemaModifications.md系统讲解create(table:)、alter(table:)、drop(table:)与索引创建等核心能力并结合仓库源码说明其底层 SQL 生成原理。读完本文你将能够在 GRDB 应用与数据库迁移中用编译器可检查的方式完成建表、改列、删表与索引维护的全部常见工作。优先使用 Swift 方法而非原始 SQL修改数据库 Schema 时GRDB 官方文档给出的首要建议是优先使用 Swift 方法而不是手写 SQL 查询。这样做的核心收益在于——Swift 方法允许编译器检查某个 Schema 变更在当前目标操作系统上是否可用把版本兼容性问题提前到编译期而非运行期暴露。只有不存在对应 Swift 方法时例如创建触发器才退回到原始 SQL 查询。这些方法的实现集中在 GRDB/QueryInterface/Schema/DatabaseSchemaDefinition.swift包括建表、改表、删表、建视图、建索引等每个方法内部都会生成对应的 SQL 并通过execute(sql:)执行。此外当一个 Schema 变更不被 SQLite 直接支持或目标操作系统上的 SQLite 版本不支持时通常意味着需要重建表——即创建新表、迁移数据、删除旧表。完整流程见 GRDB/Documentation.docc/Migrations.md。创建数据表Database.create(table:options:body:)覆盖了 SQLite 建表功能的绝大部分能力其闭包参数是一个 TableDefinition 实例用于描述表的列与约束。虚拟表virtual table请参考 Documentation/FullTextSearch.md或使用原始 SQL。基础建表示例// CREATE TABLE place ( // id INTEGER PRIMARY KEY AUTOINCREMENT, // title TEXT, // favorite BOOLEAN NOT NULL DEFAULT 0, // latitude DOUBLE NOT NULL, // longitude DOUBLE NOT NULL // ) try db.create(table: place) { t in t.autoIncrementedPrimaryKey(id) t.column(title, .text) t.column(favorite, .boolean).notNull().defaults(to: false) t.column(longitude, .double).notNull() t.column(latitude, .double).notNull() }闭包内的t即 TableDefinition它把所有列与约束收集到内部结构中最终由SQLTableGenerator见 GRDB/QueryInterface/SQLGeneration/SQLTableGenerator.swift拼接成一条或多条 SQL 语句执行。配置建表选项TableOptionscreate(table:)的options参数接收 TableOptions 选项集源码中定义了四个成员选项SQL 效果说明.ifNotExistsIF NOT EXISTS表已存在时静默跳过不报错.temporaryCREATE TEMPORARY TABLE创建临时表.withoutRowIDWITHOUT ROWID无 rowid 表无法被 doc:DatabaseObservation 等数据库观察工具跟踪.strictSTRICTSTRICT 表要求 SQLite 3.37iOS 15.4 / macOS 12.4 / tvOS 15.4 / watchOS 8.5 起// CREATE TABLE player ( ... ) try db.create(table: player) { t in ... } // CREATE TEMPORARY TABLE player IF NOT EXISTS ( try db.create(table: player, options: [.temporary, .ifNotExists]) { t in ... }从 SQLTableGenerator.swift 的 SQL 拼接逻辑可以看到TEMPORARY、IF NOT EXISTS会直接插入到CREATE TABLE关键字之后而WITHOUT ROWID、STRICT则出现在表定义末尾。添加列列名与列类型使用t.column(_:_:)添加普通列第二个参数为可选的 Database.ColumnType。Database.ColumnType在 Database.swift 中定义提供以下预置类型对应 SQL 声明ColumnTypeSQL适用场景.textTEXT文本.jsonTextTEXTJSON 文本列.jsonbBLOBJSONB 二进制列.integerINTEGER整数.doubleDOUBLE浮点数.realREAL浮点数.numericNUMERIC数值.booleanBOOLEAN布尔.blobBLOB二进制.dateDATE日期.datetimeDATETIME日期时间.anyANY无类型约束// CREATE TABLE player ( // score, // name TEXT, // creationDate DATETIME, // address TEXT, try db.create(table: player) { t in t.column(score) t.column(name, .text) t.column(creationDate, .datetime) t.column(address, .json) }其中省略类型即可让 SQLite 采用默认存储类JSON 列在 GRDB 中推荐使用.jsonTextTEXT 适合 JSON 函数与操作符或.jsonbBLOB详见 GRDB/Documentation.docc/JSON.md。NOT NULL 约束与默认值返回的 ColumnDefinition 支持链式配置// email TEXT NOT NULL, t.column(email, .text).notNull() // name TEXT NOT NULL DEFAULT Anonymous, t.column(name, .text).notNull().defaults(to: Anonymous)notNull(onConflict:)添加NOT NULL约束可附带冲突解决策略默认.abort。defaults(to:)接受任意DatabaseValueConvertible值作为默认值。defaults(sql:)接受 SQL 片段例如t.column(creationDate, .datetime).defaults(sql: CURRENT_TIMESTAMP)。主键单列与多列主键有四种定义方式均位于 TableDefinition.swift// id INTEGER PRIMARY KEY AUTOINCREMENT, t.autoIncrementedPrimaryKey(id) // uuid TEXT PRIMARY KEY NOT NULL, t.primaryKey(uuid, .text) // teamName TEXT NOT NULL, // position INTEGER NOT NULL, // PRIMARY KEY (teamName, position), t.primaryKey { t.column(teamName, .text) t.column(position, .integer) } // 等价写法表级约束形式 // PRIMARY KEY (a, b) t.primaryKey([a, b])几点实现层面的细节可以帮助你正确使用autoIncrementedPrimaryKey(id)本质是column(id, .integer).primaryKey(autoincrement: true)生成的INTEGER PRIMARY KEY AUTOINCREMENT会保证 ID 永不复用。GRDB 建表文档特别建议优先使用它因为 ID 复用会让应用或数据库观察doc:DatabaseObservation误以为行被更新而实际是被删除后替换。对于非INTEGER类型的主键t.primaryKey(uuid, .text)会自动追加NOT NULL以规避 SQLite 允许主键含 NULL 的历史怪癖源码中注释引用了 SQLite quirks 文档。在t.primaryKey { ... }闭包内定义的列同样会被自动加上NOT NULL见 TableDefinition.swift 中column(_:_:)的实现因此用t.primaryKey([a, b])这种表级约束写法时你需要手动对参与主键的列调用notNull()。每个表只能定义一个主键重复定义会在运行时触发fatalError(cant define several primary keys)。唯一键单列唯一约束直接加在列上多列唯一约束使用表级方法// email TEXT UNIQUE, t.column(email, .text).unique() // UNIQUE (a, b) ON CONFLICT REPLACE, t.uniqueKey([a, b], onConflict: .replace)unique(onConflict:)默认冲突策略为.abortuniqueKey(_:onConflict:)则可传入 Database.ConflictResolutionrollback、abort、fail、ignore、replace。外键与 belongsTo 关联定义外键时被引用的列默认是引用表的主键除非另行指定// teamId TEXT REFERENCES team(id) ON DELETE CASCADE, // countryCode TEXT REFERENCES country(code) NOT NULL, t.belongsTo(team, onDelete: .cascade) t.belongsTo(country).notNull()belongsTo 是 GRDB 的特色方法它会自动添加与引用表主键数量相同的列列名 前缀 引用表主键列名并声明外键约束。例如t.belongsTo(team)会为player表添加teamId列并引用team(id)belongsTo(country)会添加countryCode列。它的完整签名源码中均有注释说明为t.belongsTo( _ name: String, inTable table: String? nil, // 显式指定引用表自定义列前缀或自引用时使用 onDelete: Database.ForeignKeyAction? nil, // .cascade / .restrict / .setNull / .setDefault onUpdate: Database.ForeignKeyAction? nil, deferred: Bool false, // 延迟外键检查DEFERRABLE INITIALLY DEFERRED indexed: Bool true) // 是否自动为添加的列建索引默认 true更多使用要点复数表名单数名称会自动解析到复数表名例如t.belongsTo(team)在players表中引用teams(id)t.belongsTo(country)引用countries(code)。自引用t.belongsTo(captain, inTable: player)生成captainId INTEGER REFERENCES player(id)一本书有作者和译者时可写t.belongsTo(author, inTable: person)与t.belongsTo(translator, inTable: person)。自动索引默认创建的列会被自动索引indexed: true可用indexed: false关闭ForeignKeyDefinition.unique()可让该索引唯一例如每个国家只有一个玩家的约束。等价写法t.belongsTo(team)等价于t.column(teamId, .integer).references(team).indexed()也等价于t.column(teamId, .integer).indexed()t.foreignKey([teamId], references: team)。不确定自动生成的列名时可查询try db.columns(in: player).map(\.name)确认。除了belongsTo还有更底层的方法// FOREIGN KEY (a, b) REFERENCES parents(c, d), t.foreignKey([a, b], references: parents) // 列级外键可指定引用列、更新/删除动作、是否延迟 t.column(authorId, .integer).references(author, onDelete: .cascade)references(_:column:onDelete:onUpdate:deferred:)是 ColumnDefinition 上的方法column参数缺省时引用目标表主键。为列创建索引t.column(score, .integer).indexed()indexed()会创建默认命名的索引table_on_column例如player_on_score这一命名规则在 ColumnDefinition.indexDefinition 与Database.defaultIndexName中均有体现。需要更精细的索引控制唯一、部分索引、表达式索引时使用下方创建与删除索引一节中的方法。完整性检查CHECK 约束可以对单列或整个表添加CHECK约束SQLite 只允许符合条件的数据行进入// name TEXT CHECK (LENGTH(name) 0) // score INTEGER CHECK (score 0) t.column(name, .text).check { length($0) 0 } t.column(score, .integer).check(sql: score 0)闭包中的$0代表被定义列对应的 Column可以在此基础上构建任意 SQL 表达式。原始 SQL 列与 SQL 字面量列也可以用原始 SQL 字符串定义或用 SQL 字面量 安全地内嵌值——后者可避免语法错误和 SQL 注入风险t.column(sql: name TEXT) let defaultName: String ... t.column(literal: name TEXT DEFAULT \(defaultName))注意在t.primaryKey { ... }闭包内不能使用原始 SQL 定义主键列源码中以GRDBPrecondition强制校验。表级约束涉及多列的约束使用表级方法// PRIMARY KEY (a, b), t.primaryKey([a, b]) // UNIQUE (a, b) ON CONFLICT REPLACE, t.uniqueKey([a, b], onConflict: .replace) // FOREIGN KEY (a, b) REFERENCES parents(c, d), t.foreignKey([a, b], references: parents) // CHECK (a b 10), t.check(Column(a) Column(b) 10) // CHECK (a b 10) t.check(sql: a b 10) // Raw SQL constraints t.constraint(sql: CHECK (a b 10)) t.constraint(literal: CHECK (a b \(10)))生成列Generated ColumnsColumnDefinition提供generatedAs系列方法支持 SQLite 的 VIRTUAL / STORED 生成列默认 VIRTUAL可用第二个参数指定.storedt.column(totalScore, .integer).generatedAs(sql: score bonus) t.column(totalScore, .integer).generatedAs(Column(score) Column(bonus))从源码ColumnDefinition.swift看generatedAs在标准构建下标注为available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)即要求系统 SQLite 3.35.0而使用自定义 SQLiteGRDBCUSTOMSQLITE / SQLCipher时无此平台限制。生成列同样会被SELECT *返回。如果希望记录类型忽略生成列可在FetchableRecord中自定义databaseSelection例如[.allColumns(excluding: [totalScore])]。修改现有表SQLite 支持对已有表进行有限的修改GRDB 提供了rename(table:to:)与alter(table:)两个入口相关实现见 DatabaseSchemaDefinition.swift 与 TableAlteration.swift。// ALTER TABLE referer RENAME TO referrer try db.rename(table: referer, to: referrer) // ALTER TABLE player ADD COLUMN hasBonus BOOLEAN // ALTER TABLE player RENAME COLUMN url TO homeURL // ALTER TABLE player DROP COLUMN score try db.alter(table: player) { t in t.add(column: hasBonus, .boolean) t.rename(column: url, to: homeURL) t.drop(column: score) }TableAlteration 提供三种变更操作方法SQL可用性说明add(column:_:)ALTER TABLE ... ADD COLUMN全平台可用addColumn(sql:)/addColumn(literal:)ADD COLUMN原始 SQL / SQL 字面量全平台可用rename(column:to:)ALTER TABLE ... RENAME COLUMN全平台可用drop(column:)ALTER TABLE ... DROP COLUMN标准构建要求 iOS 15 / macOS 12 / tvOS 15 / watchOS 8SQLite 3.35.0自定义 SQLite 无限制其中rename(column:to:)与drop(column:)在自定义 SQLiteGRDBCUSTOMSQLITE/SQLCipher分支下没有available限制见 TableAlteration.swift 的编译分支。关于外键重命名的特别提醒在迁移中重命名外键列时官方文档建议使用DatabaseMigrator.ForeignKeyChecks.immediate立即检查而非默认的禁用外键检查模式以避免完整性失败。例如// RECOMMENDED: rename foreign keys with immediate foreign key checks. migrator.registerMigration(Guilds, foreignKeyChecks: .immediate) { db in try db.rename(table: team, to: guild) try db.alter(table: player) { t in t.rename(column: teamId, to: guildId) } }注意SQLite 对表变更能力有限制执行 ALTER 操作后可能需要重建受影响的触发器或视图详见 GRDB/Documentation.docc/Migrations.md。删除数据表try db.drop(table: obsolete)对应 SQL 为DROP TABLE obsolete实现在 DatabaseSchemaDefinition.swift。创建与删除视图Schema 修改 API 中也包含视图管理定义见 DatabaseSchemaDefinition.swift// CREATE VIEW hero AS SELECT * FROM player WHERE isHero 1 try db.create(view: hero, as: SQLRequest(literal: SELECT * FROM player WHERE isHero 1 ))create(view:options:columns:as:)接受一个SQLSubqueryable请求因此也可以用QueryInterfaceRequest构建视图create(view:options:columns:asLiteral:)接受 SQL 字面量。ViewOptions提供.ifNotExists与.temporary两个选项。删除视图使用db.drop(view: name)。迁移中推荐使用Table(player).filter(...)而非应用内的记录类型来定义视图以避免记录类型与应用代码耦合。创建与删除索引在已有表上创建列索引// CREATE INDEX index_player_on_email ON player(email) try db.create(indexOn: player, columns: [email]) // CREATE UNIQUE INDEX index_player_on_email ON player(email) try db.create(indexOn: player, columns: [email], options: .unique)create(indexOn:columns:options:condition:)使用默认索引名index_table_on_column1_column2...如需自定义名称使用create(index:on:columns:options:condition:)try db.create(index: index_player_on_email, on: player, columns: [email], options: .unique)带排序规则或表达式的索引SQLite 支持表达式索引和指定 collation通过create(index:on:expressions:options:condition:)传入 SQLExpressible 表达式// CREATE INDEX index_player_on_email ON player(email COLLATE NOCASE) try db.create( index: index_player_on_email, on: player, expressions: [Column(email).collating(.nocase)]) // CREATE INDEX index_player_on_total_score ON player(scorebonus) try db.create( index: index_player_on_total_score, on: player, expressions: [Column(score) Column(bonus)]) // CREATE INDEX index_player_on_country ON player(address - country) try db.create( index: index_player_on_country, on: player, expressions: [ JSONColumn(address)[country], ])IndexOptions提供.ifNotExists与.unique两个选项。部分索引Partial Indexcreate系列方法的condition参数可创建部分索引例如try db.create( indexOn: player, columns: [score], condition: Column(isPro) true)删除索引try db.drop(index: index_player_on_email) // 若表上恰好存在一个覆盖指定列的组合索引则删除 try db.drop(indexOn: player, columns: [email])此外reindex(collation:)可在排序规则定义变化后重建所有使用该规则Database.CollationName或自定义DatabaseCollation的索引。唯一约束与唯一索引并不完全等价——例如唯一约束与关联自动索引的命名和冲突行为存在差异。GRDB 在 GRDB/Documentation.docc/DatabaseSchemaRecommendations.md 的 Unique keys should be supported by unique indexes 一节专门给出了建议值得一读。Schema 修改 API 一览以下是本文档Topics 部分列出的全部相关 API均已在源码中实现数据库表Database/alter(table:body:)、Database/create(table:options:body:)Database/create(virtualTable:options:using:)/Database/create(virtualTable:options:using:_:)虚拟表参见 Documentation/FullTextSearch.mdDatabase/drop(table:)、Database/rename(table:to:)Database/dropFTS4SynchronizationTriggers(forTable:)/dropFTS5SynchronizationTriggers(forTable:)FTS 同步触发器Database/ColumnType、Database/ConflictResolution、Database/ForeignKeyActionTableAlteration、TableDefinition、TableOptions、VirtualTableModule、VirtualTableOptions数据库视图Database/create(view:options:columns:as:)、create(view:options:columns:asLiteral:)、drop(view:)、ViewOptions数据库索引Database/create(indexOn:columns:options:condition:)Database/create(index:on:columns:options:condition:)Database/create(index:on:expressions:options:condition:)Database/drop(indexOn:columns:)、Database/drop(index:)、IndexOptions已废弃Sunsetted方法为向后兼容保留的旧接口create(index:on:columns:unique:ifNotExists:condition:)、create(table:temporary:ifNotExists:withoutRowID:body:)、create(virtualTable:ifNotExists:using:)等不推荐在新代码中使用。源码实现要点从 TableDefinition 到 SQL 生成理解 GRDB 的 Schema 修改 API 有助于写出更符合预期的代码构建-生成-执行三段式create(table:)、alter(table:)、create(index:...)都是先构建 DSL 对象TableDefinition/TableAlteration/IndexDefinition再由对应的 SQL 生成器SQLTableGenerator.swift、SQLTableAlterationGenerator、SQLIndexGenerator拼出完整 SQL最后统一执行。前向主键解析SQLTableGenerator在处理自引用外键时需要知道主键列名而目标表可能尚未落库因此它维护了前向主键列forward primary key columns——这也解释了为什么belongsTo(captain, inTable: player)这类自引用可以在建表闭包内直接声明。主键自动 NOT NULL针对 SQLite PRIMARY KEY 可能含 NULL 的历史行为GRDB 在primaryKey(_:_:)、primaryKey(body:)内部自动追加notNull()这是 TableDefinition.swift 中刻意设计的防御性逻辑。约束冲突策略ConflictResolution与ForeignKeyAction都是枚举Database.swift其rawValue直接作为 SQL 关键字如ON CONFLICT REPLACE、ON DELETE CASCADE拼入语句保证生成 SQL 的确定性。与迁移Migrations的衔接这些 Schema 修改方法的最佳实践场景是配合 DatabaseMigrator 使用。当 SQLite 无法直接完成某个 Schema 变更例如修改列的语义、无法ADD COLUMN带约束的新列等时需要走建新表 → 复制数据 → 删旧表 → 重命名的重建流程完整的操作步骤与注意事项见 GRDB/Documentation.docc/Migrations.md。另外Database.clearSchemaCache()见 DatabaseSchema.swift可以在外部连接修改了同一数据库文件的 Schema 后刷新 GRDB 的 Schema 缓存避免使用过期的元数据。【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →