Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/AppDatabase.swift
T
2025-01-22 14:09:10 +08:00

236 lines
8.0 KiB
Swift

import Foundation
import GRDB
import os.log
/// A database of players.
///
/// You create an `AppDatabase` with a connection to an SQLite database
/// (see <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>).
///
/// Create those connections with a configuration returned from
/// `AppDatabase/makeConfiguration(_:)`.
///
/// For example:
///
/// ```swift
/// // Create an in-memory AppDatabase
/// let config = AppDatabase.makeConfiguration()
/// let dbQueue = try DatabaseQueue(configuration: config)
/// let appDatabase = try AppDatabase(dbQueue)
/// ```
struct AppDatabase {
/// Creates an `AppDatabase`, and makes sure the database schema
/// is ready.
///
/// - important: Create the `DatabaseWriter` with a configuration
/// returned by ``makeConfiguration(_:)``.
init(_ dbWriter: any DatabaseWriter) throws {
self.dbWriter = dbWriter
try migrator.migrate(dbWriter)
}
/// Provides access to the database.
///
/// Application can use a `DatabasePool`, while SwiftUI previews and tests
/// can use a fast in-memory `DatabaseQueue`.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections>
private let dbWriter: any DatabaseWriter
}
// MARK: - Database Configuration
extension AppDatabase {
private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL")
/// Returns a database configuration suited for `PlayerRepository`.
///
/// SQL statements are logged if the `SQL_TRACE` environment variable
/// is set.
///
/// - parameter base: A base configuration.
public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration {
var config = base
// An opportunity to add required custom SQL functions or
// collations, if needed:
// config.prepareDatabase { db in
// db.add(function: ...)
// }
// Log SQL statements if the `SQL_TRACE` environment variable is set.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/database/trace(options:_:)>
if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil {
config.prepareDatabase { db in
db.trace {
// It's ok to log statements publicly. Sensitive
// information (statement arguments) are not logged
// unless config.publicStatementArguments is set
// (see below).
os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0))
}
}
}
#if DEBUG
// Protect sensitive information by enabling verbose debugging in
// DEBUG builds only.
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/configuration/publicstatementarguments>
config.publicStatementArguments = true
#endif
return config
}
}
// MARK: - Database Migrations
extension AppDatabase {
/// The DatabaseMigrator that defines the database schema.
///
/// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
private var migrator: DatabaseMigrator {
var migrator = DatabaseMigrator()
#if DEBUG
// Speed up development by nuking the database when migrations change
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations>
migrator.eraseDatabaseOnSchemaChange = true
#endif
migrator.registerMigration("createPlayer") { db in
// Create a table
// See <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseschema>
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
// Migrations for future application versions will be inserted here:
// migrator.registerMigration(...) { db in
// ...
// }
return migrator
}
}
// MARK: - Database Access: Writes
// The write methods execute invariant-preserving database transactions.
extension AppDatabase {
/// A validation error that prevents some players from being saved into
/// the database.
enum ValidationError: LocalizedError {
case missingName
var errorDescription: String? {
switch self {
case .missingName:
return "Please provide a name"
}
}
}
/// Saves (inserts or updates) a player. When the method returns, the
/// player is present in the database, and its id is not nil.
func savePlayer(_ player: inout Player) async throws {
if player.name.isEmpty {
throw ValidationError.missingName
}
player = try await dbWriter.write { [player] db in
try player.saved(db)
}
}
/// Delete the specified players
func deletePlayers(ids: [Int64]) async throws {
try await dbWriter.write { db in
_ = try Player.deleteAll(db, ids: ids)
}
}
/// Delete all players
func deleteAllPlayers() async throws {
try await dbWriter.write { db in
_ = try Player.deleteAll(db)
}
}
/// Refresh all players (by performing some random changes, for demo purpose).
func refreshPlayers() async throws {
try await dbWriter.write { db in
if try Player.all().isEmpty(db) {
// When database is empty, insert new random players
try createRandomPlayers(db)
} else {
// Insert a player
if Bool.random() {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
// Delete a random player
if Bool.random() {
try Player.order(sql: "RANDOM()").limit(1).deleteAll(db)
}
// Update some players
for var player in try Player.fetchAll(db) where Bool.random() {
try player.updateChanges(db) {
$0.score = Player.randomScore()
}
}
}
}
}
/// Create random players if the database is empty.
func createRandomPlayersIfEmpty() throws {
try dbWriter.write { db in
if try Player.all().isEmpty(db) {
try createRandomPlayers(db)
}
}
}
private static let uiTestPlayers = [
Player(id: nil, name: "Arthur", score: 5),
Player(id: nil, name: "Barbara", score: 6),
Player(id: nil, name: "Craig", score: 8),
Player(id: nil, name: "David", score: 4),
Player(id: nil, name: "Elena", score: 1),
Player(id: nil, name: "Frederik", score: 2),
Player(id: nil, name: "Gilbert", score: 7),
Player(id: nil, name: "Henriette", score: 3)]
func createPlayersForUITests() throws {
try dbWriter.write { db in
try AppDatabase.uiTestPlayers.forEach { player in
_ = try player.inserted(db) // insert but ignore inserted id
}
}
}
/// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`.
private func createRandomPlayers(_ db: Database) throws {
for _ in 0..<8 {
_ = try Player.makeRandom().inserted(db) // insert but ignore inserted id
}
}
}
// MARK: - Database Access: Reads
// This demo app does not provide any specific reading method, and instead
// gives an unrestricted read-only access to the rest of the application.
// In your app, you are free to choose another path, and define focused
// reading methods.
extension AppDatabase {
/// Provides a read-only access to the database
var reader: DatabaseReader {
dbWriter
}
}