-
-
Notifications
You must be signed in to change notification settings - Fork 759
What's the "right" way to use autoIncrementedPrimaryKey
?
#1435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Hello @OscarApeland, I think you'll find GRBD documentation that is slightly outdated about this, so your question is a perfect opportunity to clarify ideas about SQLite auto-incremented ids. It will be easier to update the doc after that. The recommended type for auto-incremented ids is the optional
|
That clarified everything I was wondering about and more - thank you very much! |
Great answer! Would be great to add to the documentation. In this example it's easy to keep the properties of public protocol Persistable: Codable, FetchableRecord, MutablePersistableRecord {
associatedtype ID: Hashable, DatabaseValueConvertible
static var databaseTableName: String { get }
}
public struct Persisted<V>: Identifiable, FetchableRecord, MutablePersistableRecord where V: Persistable {
static public var databaseTableName: String { V.databaseTableName }
public typealias ID = V.ID
public var id: V.ID
public var value: V
public init(row: GRDB.Row) throws {
id = row[Column("id")]
value = try V(row: row)
}
public func encode(to container: inout GRDB.PersistenceContainer) throws {
container[Column("id")] = id
try value.encode(to: &container)
}
}
struct Player: Persistable {
typealias ID = Tagged<Player, Int64>
var name: String
var score: Int
}
typealias PersistedPlayer = Persisted<Player>
func foo(dbQueue: DatabaseQueue) throws {
try dbQueue.write { db in
// Insert
var player = Player(name: "Arthur", score: 100)
let persistedPlayer = try player.insertAndFetch(db, as: PersistedPlayer.self)!
let persistedName = persistedPlayer.value.name
}
} |
Sure @johankool, go ahead if your app can profit from this For extra convenience, you may want to make it @dynamicMemberLookup
struct Persisted<V> ... {
subscript<T>(dynamicMember keyPath: KeyPath<V, T>) -> T {
value[keyPath: keyPath]
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<V, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
}
// No `value` in sight!
let persistedName = persistedPlayer.name |
To clarify, does the try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("player")
} then we could have a different property for the ID. Maybe something like this: struct Player: Identifiable, Codable, PersistableRecord {
var player: Int64?
private let _uuid: UUID
enum Identifier: Codable, Hashable {
case db(Int64)
case temp(UUID)
}
// Use a random UUID as the id until the db creates an auto incrementing primary key
var id: Self.Identifier {
guard let player else { return .temp(self._uuid) }
return .db(player)
}
} Would GRDB/SQLite still have a conflict even if we used an approach like this? I guess my question is, is there a way that we could call |
Hello @DandyLyons, Using a custom identifier type is fruitful idea, yes. I've performed a few similar attempts but did not find anything that could reach the level of a recommended technique. Probably I didn't try hard enough, and fresh ideas are welcome. It's good that you can make something work in your app. Apps are the best place for exploring new techniques :-) I'm not personally found of toying with the schema itself, and by that I mean that I'd try to find a solution that has no impact on the schema (so that people can name their column "id" if that's what they feel like doing). If your And if it provided a custom implementation of
You can call You can also add an extension: // App code
extension Player {
static func find(_ db: Database, id: Int64) throws -> Player {
try Player.find(db, key: rowid)
}
} If you want to share this extension between multiple record types, define a protocol. If you want to discuss this, please open a new discussion. That's because I'd like this issue to remain focused on answers and solutions, rather than unbounded explorations. |
thanks to @groue , @OscarApeland and @johankool for this thread which helped me greatly. I wanted to offer the solution I'm using in case any future interloper finds it helpful. I wanted the reverse concept for my two models; so instead of X and PersistedX, I went for UnsavedX and X... The protocol / generic: public protocol Savable:
Codable,
Hashable,
FetchableRecord,
PersistableRecord,
Sendable
{}
@dynamicMemberLookup
public struct Saved<V>:
Codable,
Hashable,
Identifiable,
FetchableRecord,
PersistableRecord,
Sendable
where V: Savable {
public var id: Int64
public var value: V
public static var databaseTableName: String { V.databaseTableName }
subscript<T>(dynamicMember keyPath: KeyPath<V, T>) -> T {
value[keyPath: keyPath]
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<V, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
public init(row: GRDB.Row) throws {
id = row[Column("id")]
value = try V(row: row)
}
public func encode(to container: inout GRDB.PersistenceContainer) throws {
container[Column("id")] = id
try value.encode(to: &container)
}
} The model: struct UnsavedPodcast: Savable {
let feedURL: URL
var title: String
static var databaseTableName: String { "podcast" }
}
typealias Podcast = Saved<UnsavedPodcast> the tests: @Test("that a podcast can be created, fetched, updated, and deleted")
func createSinglePodcast() async throws {
let url = URL(string: "https://example.com/data")!
try db.write { db in
let unsavedPodcast = UnsavedPodcast(feedURL: url, title: "Title")
var podcast = try unsavedPodcast.insertAndFetch(db, as: Podcast.self)
#expect(podcast.title == unsavedPodcast.title)
let fetchedPodcast = try Podcast.find(db, id: podcast.id)
#expect(fetchedPodcast == podcast)
let filteredPodcast =
try Podcast.filter(Column("title") == podcast.title).fetchOne(db)
#expect(filteredPodcast == podcast)
podcast.title = "New Title"
try podcast.update(db)
let fetchedUpdatedPodcast = try Podcast.find(db, id: podcast.id)
#expect(fetchedUpdatedPodcast == podcast)
let updatedFilteredPodcast =
try Podcast.filter(Column("title") == podcast.title).fetchOne(db)
#expect(updatedFilteredPodcast == podcast)
let urlFilteredPodcast =
try Podcast.filter(Column("feedURL") == url).fetchOne(db)
#expect(urlFilteredPodcast == podcast)
let fetchedAllPodcasts = try Podcast.fetchAll(db)
#expect(fetchedAllPodcasts == [podcast])
#expect(try podcast.exists(db))
let deleted = try podcast.delete(db)
#expect(deleted)
#expect(!(try podcast.exists(db)))
let noPodcasts = try Podcast.fetchAll(db)
#expect(noPodcasts.isEmpty)
let allCount = try Podcast.fetchCount(db)
#expect(allCount == 0)
let titleCount =
try Podcast.filter(Column("title") == podcast.title).fetchCount(db)
#expect(titleCount == 0)
}
} |
perhaps also adding: extension Savable {
static var databaseTableName: String {
let prefix = "Unsaved"
let typeName =
String(describing: Self.self).components(separatedBy: ".").last ?? ""
guard typeName.hasPrefix(prefix) else {
fatalError("Struct name: \(typeName) must start with \"\(prefix)\".")
}
let suffix = typeName.dropFirst(prefix.count)
guard let firstCharacter = suffix.first else {
fatalError("Struct name after '\(prefix)' prefix is empty.")
}
let tableName = firstCharacter.lowercased() + suffix.dropFirst()
return tableName
}
} so that my implementation can become as clean as: struct UnsavedPodcast: Savable {
let feedURL: URL
var title: String
}
typealias Podcast = Saved<UnsavedPodcast> |
Thank you for sharing your solution, @jubishop 👍 Applications can indeed define their own protocols when they want several of their record types to share a common behavior. Another example can be found in the Record Timestamps and Transaction Date guide: |
I just implemented Tagged into my codebase, I added import Foundation
import GRDB
import Tagged
extension Tagged: @retroactive SQLExpressible
where RawValue: SQLExpressible {}
extension Tagged: @retroactive StatementBinding
where RawValue: StatementBinding {}
extension Tagged: @retroactive StatementColumnConvertible
where RawValue: StatementColumnConvertible {}
extension Tagged: @retroactive DatabaseValueConvertible
where RawValue: DatabaseValueConvertible {} per your advice given here in sept 2023. does this remain the same for fully up to date grab 7.0 here in early 2025? |
Yes @jubishop, that's what I do in my apps that use GRDB 7 beta 👍 |
Uh oh!
There was an error while loading. Please reload this page.
Fresh to GRDB. Loving it so far.
I can't seem to find documentation on the preferred method for creating objects with auto-incrementing IDs. Lets say I'm creating a table like this.
First of all, should this property be
Int
orInt64
? Then, how should the property be defined in the corresponding record?It seems like it needs to be either optional or implicitly-unwrapped-optional, because just initing with
Row(id: 0).insertAndFetch(db)
crashes due to non-unique IDs. Initing the model withRow(id: nil).insertAndFetch(db)
works, but that requires the Optional/IUO.Is using IUO's the intended method for creating objects with auto-incremented IDs?
The text was updated successfully, but these errors were encountered: