PersistableRecord is a protocol for types that can insert, update, and save themselves to the database.
Overview
Conform to PersistableRecord to persist your custom types:
struct Player: PersistableRecord, Encodable {
var id: Int64?
var name: String
var score: Int
}
try dbQueue.write { db in
var player = Player(id: nil, name: "Arthur", score: 100)
try player.insert(db)
print(player.id) // Auto-incremented ID
}
Encodable types get automatic PersistableRecord conformance with minimal setup.
Encodable types automatically conform:
struct Player: PersistableRecord, Encodable {
var id: Int64?
var name: String
var score: Int
}
Implement the encode(to:) method:
struct Player: PersistableRecord {
var id: Int64?
var name: String
var score: Int
func encode(to container: inout PersistenceContainer) {
container["id"] = id
container["name"] = name
container["score"] = score
}
}
Configuring the Table
Combine with TableRecord to specify the table name:
struct Player: PersistableRecord, TableRecord, Encodable {
static let databaseTableName = "player"
var id: Int64?
var name: String
var score: Int
}
Inserting Records
insert(_:onConflict:)
Inserts a record into the database.
onConflict
Database.ConflictResolution?
default:"nil"
Conflict resolution strategy
var player = Player(id: nil, name: "Arthur", score: 100)
try player.insert(db)
// Insert with conflict resolution
try player.insert(db, onConflict: .ignore)
For structs, insert is mutating and updates the id property with the auto-incremented value.
insertAndFetch(_:onConflict:as:)
Inserts a record and fetches the inserted row.
var player = Player(id: nil, name: "Arthur", score: 100)
let inserted = try player.insertAndFetch(db)
print(inserted.id) // Auto-incremented ID
Updating Records
update(_:onConflict:)
Updates a record in the database.
var player = Player(id: 1, name: "Arthur", score: 100)
player.score = 150
try player.update(db)
Throws a RecordError.recordNotFound error if the record doesn’t exist in the database.
update(_:onConflict:columns:)
Updates specific columns only.
var player = Player(id: 1, name: "Arthur", score: 150)
try player.update(db, columns: ["score"])
// Only score is updated, name is unchanged in database
Saving Records
save(_:onConflict:)
Inserts or updates a record depending on whether it exists.
var player = Player(id: nil, name: "Arthur", score: 100)
try player.save(db) // Inserts
player.score = 150
try player.save(db) // Updates
saveAndFetch(_:onConflict:as:)
Saves a record and fetches the saved row.
var player = Player(id: nil, name: "Arthur", score: 100)
let saved = try player.saveAndFetch(db)
print(saved.id)
Upserting Records
upsert(_:)
Inserts or updates using SQLite’s UPSERT (INSERT … ON CONFLICT DO UPDATE).
let player = Player(id: 1, name: "Arthur", score: 100)
try player.upsert(db)
Requires SQLite 3.24.0+ and a conflict target (usually a primary key or unique constraint).
upsertAndFetch(_:onConflict:updating:doUpdate:)
Upserts and fetches the result.
let player = Player(id: 1, name: "Arthur", score: 100)
let upserted = try player.upsertAndFetch(db)
Deleting Records
delete(_:)
Deletes a record from the database.
let player = Player(id: 1, name: "Arthur", score: 100)
let deleted = try player.delete(db)
print(deleted) // true if deleted, false if not found
Persistence Callbacks
willInsert(_:)
Called before the record is inserted.
struct Player: PersistableRecord {
var id: Int64?
var name: String
var createdAt: Date
func willInsert(_ db: Database) throws {
// Set createdAt before insertion
createdAt = Date()
}
}
didInsert(_:)
Called after successful insertion.
Contains information about the inserted row
struct Player: PersistableRecord {
var id: Int64?
var name: String
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
willUpdate(_:columns:)
Called before the record is updated.
struct Player: PersistableRecord {
var id: Int64
var name: String
var updatedAt: Date
func willUpdate(_ db: Database, columns: Set<String>) throws {
updatedAt = Date()
}
}
didUpdate(_:)
Called after successful update.
func didUpdate(_ updated: UpdateSuccess) {
print("Updated \(updated.changesCount) columns")
}
willSave(_:)
Called before save (insert or update).
func willSave(_ db: Database) throws {
// Validation logic
guard !name.isEmpty else {
throw ValidationError.emptyName
}
}
didSave(_:)
Called after successful save.
func didSave(_ saved: SaveSuccess) {
print("Saved with \(saved.changesCount) changes")
}
willDelete(_:)
Called before deletion.
func willDelete(_ db: Database) throws {
// Clean up related data
}
didDelete(_:)
Called after successful deletion.
func didDelete(_ deleted: DeletionSuccess) {
print("Deleted: \(deleted.deleted)")
}
Conflict Resolution
try player.insert(db, onConflict: .ignore) // INSERT OR IGNORE
try player.insert(db, onConflict: .replace) // INSERT OR REPLACE
try player.update(db, onConflict: .rollback) // UPDATE OR ROLLBACK
try player.update(db, onConflict: .abort) // UPDATE OR ABORT
try player.update(db, onConflict: .fail) // UPDATE OR FAIL
Customizing Encodable Encoding
Column Name Encoding
struct Player: PersistableRecord, Encodable {
static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
var playerID: Int64 // Encoded to player_id
var fullName: String // Encoded to full_name
}
Date Encoding
struct Player: PersistableRecord, Encodable {
static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy {
.timeIntervalSince1970
}
var createdAt: Date // Stored as epoch timestamp
}
Data Encoding
struct Player: PersistableRecord, Encodable {
static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy {
.deferredToData
}
var avatarData: Data
}
JSON Encoding
struct Player: PersistableRecord, Encodable {
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}
var metadata: Metadata // Encoded as JSON
}
Batch Operations
Combine with TableRecord for batch operations:
// Delete all players with score < 100
try Player.filter(Column("score") < 100).deleteAll(db)
// Update all players
try Player.updateAll(db) { player in
[player.score.set(to: 0)]
}
See Also