Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

With GRDB, use raw sql BUT consume the results in the convenience form?

Tags:

swift

grdb

Here's an example SQL query which you can't build using the convenience builders in GRDB...

let q = "job.id, job.name, job.city, 

ifnull(jobcategory.value, 'no category'),
ifnull(jobpriority.value, 'no priority'),

from job  

left join jobcategory on job.category = jobcategory.id
left join jobpriority on job.priority = jobpriority.id

where job.user = 13"

(indeed, I made an example you can't build in any of the older, less supported iOS Swift SQL libraries either)

and then something like

for ..
 let id = ..
 let name = ..
 let city = ..
 let category = ..
 let priority = ..

I have heard that with GRDB, you can actually use the raw SQL (i.e., actually not even use the query builder of GRDB), but, in the result still use the convenience consumption*, with care for types, etc.

If so, in fact how do you do this in the example?

like image 994
Fattie Avatar asked Apr 29 '17 15:04

Fattie


1 Answers

GRDB provides a query builder:

let persons = try Person.filter(emailColumn != nil).fetchAll(db) // [Person]

And it also understands SQL:

let persons = try Person.fetchAll(db, "SELECT * FROM persons WHERE email IS NOT NULL")

Both code snippets above turn database rows into Person instances. Such conversion is supported by the RowConvertible protocol and the full-featured Record class provided by GRDB. The code below uses the protocol:

struct Person {
    let email: String
    let name: String
}

extension Person : RowConvertible {
    init(row: Row) {
        email = row.value(named: "email")
        name = row.value(named: "name")
    }
}

The init(row:) constructor is used for both the "query interface" request Person.filter(...).fetchAll(db), and the SQL request Person.fetchAll(db, "SELECT ...").

That's what is meant by GRDB does not punish you when you want to use raw SQL. Your custom record types support both query interface requests and SQL requests out of the box. It is just as easy to fetch records with both techniques:

// Two one-liners:
let persons = try Person.filter(emailColumn != nil).fetchAll(db)
let persons = try Person.fetchAll(db, "SELECT * FROM persons WHERE email IS NOT NULL")

Now, your example can be written as:

struct Job {
    let id: Int64
    let name: String
    let city: String
    let category: String
    let priority: String
}

extension Job : RowConvertible {
    init(row: Row) {
        id = row.value(named: "id")
        name = row.value(named: "name")
        city = row.value(named: "city")
        category = row.value(named: "category")
        priority = row.value(named: "priority")
    }
}

try dbQueue.inDatabase { db in
    let q = "SELECT job.id, job.name, job.city, " +
        " IFNULL(jobcategory.value, 'no category') AS category, " +
        " IFNULL(jobpriority.value, 'no priority') AS priority " +
        "FROM job " +
        "LEFT JOIN jobcategory ON job.category = jobcategory.id " +
        "LEFT JOIN jobpriority ON job.priority = jobpriority.id " +
        "WHERE job.user = 13"

    let jobs = try Job.fetchAll(db, q)
}

Since category and priority are not columns of jobs, you may prefer splitting the above struct in two:

struct Job {
    let id: Int64
    let name: String
    let city: String
}

struct ExtendedJob {
    let job: Job
    let category: String
    let priority: String
}

extension Job : RowConvertible {
    init(row: Row) {
        id = row.value(named: "id")
        name = row.value(named: "name")
        city = row.value(named: "city")
    }
}

extension ExtendedJob : RowConvertible {
    init(row: Row) {
        job = Job(row: row)
        category = row.value(named: "category")
        priority = row.value(named: "priority")
    }
}

try dbQueue.inDatabase { db in
    let q = "SELECT job.id, job.name, job.city, " +
        " IFNULL(jobcategory.value, 'no category') AS category, " +
        " IFNULL(jobpriority.value, 'no priority') AS priority " +
        "FROM job " +
        "LEFT JOIN jobcategory ON job.category = jobcategory.id " +
        "LEFT JOIN jobpriority ON job.priority = jobpriority.id " +
        "WHERE job.user = 13"

    let jobs = try ExtendedJob.fetchAll(db, q)
}

You can finally encapsulate the custom SQL query in a "custom requests":

extension ExtendedJob {
    static func filter(userId: Int64) -> AnyTypedRequest<ExtendedJob> {
        let request = SQLRequest(
            "SELECT job.id, job.name, job.city, " +
            " IFNULL(jobcategory.value, 'no category') AS category, " +
            " IFNULL(jobpriority.value, 'no priority') AS priority " +
            "FROM job " +
            "LEFT JOIN jobcategory ON job.category = jobcategory.id " +
            "LEFT JOIN jobpriority ON job.priority = jobpriority.id " +
            "WHERE job.user = ?",
            arguments: [userId])
        return request.asRequest(of: ExtendedJob.self)
    }
}

// No SQL in sight:
let jobs = try dbQueue.inDatabase { db in
    try ExtendedJob.filter(userId: 13).fetchAll(db)
}
like image 142
Gwendal Roué Avatar answered Nov 11 '22 20:11

Gwendal Roué