-
Hi there 👋 I'm trying to use an SQL view as a read-only table, so that I can read from it and associate it with other tables. This partially works, but I can't seem to figure out which protocols my model needs, or how I need to set this up. A simplified version of what I have: try db.create(table: "player") { table in
table.autoIncrementedPrimaryKey("id")
table.column("name", .text).notNull()
}
try db.create(table: "goal") { table in
table.autoIncrementedPrimaryKey("id")
table.belongsTo("player")
}
try db.create(view: "playerScore", asLiteral: """
SELECT
playerId as id, --- Added since GRDB seems to require an `id`-column with unique values for `TableRecord`
playerId,
COUNT(*) as count
FROM goals GROUP BY playerId
""")
struct Player: Identifiable, Equatable, Codable, FetchableRecord, MutablePersistableRecord {
var id: Int64?
var name: String
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
static let score = hasOne(Score.self)
var score: QueryInterfaceRequest<Score> { request(for: Player.score) }
}
struct Score: Identifiable, Equatable, Decodable, FetchableRecord, TableRecord {
var id: Int64
var playerId: Int64
var count: Int
static let player = belongsTo(Player.self)
} Using this I can fetch and filter data from the table without issues: let scores = try! await self.database.reader.read { db in
try! Score.filter(Column("count") > 3).order(Column("count")).fetchAll(db)
} However, I can't use this record as an association: struct PlayerInfo: Decodable, FetchableRecord {
var player: Player
var score: Score
}
let res = try! await self.database.reader.read { db in
try! Player.including(optional: Player.score).asRequest(of: PlayerInfo.self).fetchAll(db)
}
// => Fatal error: 'try!' expression unexpectedly raised an error: SQLite error 1: no such table: score
I imagine that I don't need/want to apply `TableRecord` to my `Score` model, since it isn't a table. But I need that to have the association. Is what I want to do possible with GRDB? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 5 replies
-
Hello @robbevp, Let's start from the error message:
The reason why you see this message is that But your schema has no struct Player: Identifiable, Equatable, Codable, FetchableRecord, MutablePersistableRecord {
var id: Int64?
var name: String
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
// 👉 There is no such thing as a "foreign key" between
// an SQLite table and an SQLite view. So we have to fully
// instruct GRDB of the relationship. The `hasOne` association
// wants a foreign key from the referrer to the referred:
// playerScore.id to player.id.
static let scoreForeignKey = ForeignKey(["id"], to: ["id"])
static let score = hasOne(Score.self, using: scoreForeignKey)
var score: QueryInterfaceRequest<Score> { request(for: Player.score) }
}
struct Score: Identifiable, Equatable, Decodable, FetchableRecord, TableRecord {
var id: Int64
var playerId: Int64
var count: Int
// 👉 Replace the default table name "score"
static let databaseTableName = "playerScore"
} And now it should work: struct PlayerInfo: Decodable, FetchableRecord {
var player: Player
var score: Score? // 👉 optional because some players have no registered goal
}
// SELECT player.*, playerScore.*
// FROM player
// JOIN playerScore ON playerScore.id = player.id
try Player
.including(optional: Player.score)
.asRequest(of: PlayerInfo.self)
.fetchAll(db) Finally, regarding your comment in the view definition:
GRDB does not require anything. But your app defines If you prefer
// Variant
let dbQueue = try DatabaseQueue()
try dbQueue.write { db in
try db.create(table: "player") { table in
table.autoIncrementedPrimaryKey("id")
table.column("name", .text).notNull()
}
try db.create(table: "goal") { table in
table.autoIncrementedPrimaryKey("id")
table.belongsTo("player")
}
try db.create(
view: "playerScore",
asLiteral: "SELECT playerId, COUNT(*) AS count FROM goal GROUP BY playerId")
}
struct Player: Identifiable, Equatable, Codable, FetchableRecord, MutablePersistableRecord {
var id: Int64?
var name: String
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
// 👉 There is no such thing as a "foreign key" between
// an SQLite table and an SQLite view. So we have to fully
// instruct GRDB of the relationship. The `hasOne` association
// wants a foreign key from the referrer to the referred:
// playerScore.playerId to player.id.
static let scoreForeignKey = ForeignKey(["playerId"], to: ["id"])
static let score = hasOne(Score.self, using: scoreForeignKey)
var score: QueryInterfaceRequest<Score> { request(for: Player.score) }
}
struct Score: Equatable, Decodable, FetchableRecord, TableRecord {
var playerId: Int64
var count: Int
// 👉 Replace the default table name "score"
static let databaseTableName = "playerScore"
}
// 👉 If Identifiable conformance is needed, add it in Swift.
// It's better to address Swift concerns... in Swift ;-)
// This is not a database schema concern.
// extension Score: Identifiable {
// var id: Int64 { playerId }
// }
struct PlayerInfo: Decodable, FetchableRecord {
var player: Player
var score: Score? // 👉 optional because some players have no registered goal
}
let res = try dbQueue.read { db in
// SELECT player.*, playerScore.*
// FROM player
// LEFT JOIN playerScore ON playerScore.playerId = player.id
try Player
.including(optional: Player.score)
.asRequest(of: PlayerInfo.self)
.fetchAll(db)
} |
Beta Was this translation helpful? Give feedback.
Hello @robbevp,
Let's start from the error message:
The reason why you see this message is that
Player.including(optional: Player.score)
usesPlayer.score
, an association fromPlayer
toScore
. GRDB thus looks for the table forScore
, which, by default, is named like the record type itself:score
.But your schema has no
score
table. Instead it has aplayerScore
view. The fact that it is a view is not a problem (as long as we don't try to write into it). But we need to help GRDB making the link between the table and the view. See the "👉" comments below: