In Kitura 2.0, we integrated Codable Routing, which facilitates the creation of REST APIs and the sharing of Swift types (structs and classes) that implement Codable between iOS clients and a Kitura server. However there is a further step that is often required: persisting the same data types directly into a database, ideally whilst maintaining the type-safety.

An Object-Relational Mapping (ORM) enables you to do just that by interacting with the database directly from your Swift types.

The Swift-Kuery-ORM let’s you do just that, storing the data in a SQL relational database in a few easy steps. Any database supported by Swift-Kuery can be used, which is currently PostgreSQL and MySQL.

Why use Swift-Kuery-ORM?

Unlike other ORMs, which require you to manually set up database tables, models, and write SQL queries, Swift-Kuery-ORM doesn’t require you to directly specify how the data should be represented in the database.

You define your Swift type as a class or a struct, extend it to conform to the Model protocol (which requires no additional code) and Swift-Kuery-ORM automatically generates a table schema for you. You can then save, fetch, update and delete directly by calling functions on your Swift type.

How to use Swift-Kuery-ORM

For a full example of how to use the ORM, see the README in the Swift-Kuery-ORM project on GitHub.

Create Models

In order to use a Swift type with the ORM, you only need to make it conform to the Model protocol:

struct Student: Model {
  var name: String
  var course: String
  var grade: Int
}

The Model protocol also requires that the type conform to Codable in order to be able to convert the type to and from data as its stored and retrieved from the database. If you are sharing your Student type between an iOS client and a Kitura server, you would declare the Student type as conforming only to Codable:

struct Student: Codable {
  var name: String
  var course: String
  var grade: Int
}

and then extend Student on the server only to also conform to Model:

struct Student: Model { }

Initialising the Database

There are two mechanisms for connecting to the database: a single connection for synchronous requests, and using a connection pool for parallel asynchronous requests. Both are covered below, but it’s likely that you’ll want to use a connection pool for greater performance and concurrency.

Single Connection

To create a single connection for synchronous requests you create a database connection instance, in this case for PostgreSQL, and then set the Database.default to a Database instance using that connection:

let connection = PostgreSQLConnection(host: host, port: port, options: [ConnectionOptions]?)
Database.default = Database(single: connection)

By setting the connection as the Database.default value, all of your Model types will automatically use that connection with no additional setup.

Connection Pool

In order to create a connection pool for asynchronous requests, you create a pool connection for the PostgreSQL database instead:

let pool = PostgreSQLConnection.createPool(host: host, port: port, 
                                           options: [ConnectionOptions]?, 
                                           poolOptions: ConnectionPoolOptions)
Database.default = Database(pool: pool)

As the default database, this will again be used automatically for all of the Models.

Create Tables

Before you can store a given Model in the database, you need to create a database table in which to store it. This only needs to be done once using a call to createTable() or createTableSync() on your type.

do {
  try Student.createTableSync()
} catch {
  // Error
}

This automatically creates a table with the correct column names and types in the SQL database using the field names and types from your Model object.

createTableSync() runs synchronously so is ideal for use during server startup, whereas createTable() is asynchronous and accepts a completion handler callback.

Persisting your data

Once you have a database connection and a table created, you can save, fetch, update and delete entries in the database just by making function calls on an instance of the Model or on the Model type.

In order to save an instance of our Student struct to the database, we create an instance and call save() on that instance:

let student = Student(name: "John", course: "Computer Science", grade: 78)

// Save your student to the database.
student.save { student, error in
  ...
}

The save call takes a completion handler which runs when the database call is complete. The callback is passed the created entry or an error.

A similar approach is used to retrieve all or delete all of the entries in the database, except this uses a static call on the Student type rather than on an instance.

// Get an array of students in the database.
Student.findAll { students, error in
  ...
}

// Delete all students in the database.
Student.deleteAll { error in
  ...
}

These calls again take a completion handler that is called once the operation completes.

Working with the ORM in Kitura

Swift-Kuery-ORM really shines when used with Kitura, in particular because the ORM API has been aligned with Kitura’s Codable Routing APIs, allowing the completion handlers from the Codable Routes to be passed directly into the ORM calls. This means that no additional code needs to be written.

Saving

In order to create a REST API to allow a client to save a Student using Kitura’s Codable Routing, you would do the following:

func saveStudents(student: Student, 
                  completion: @escaping (Student?, RequestError?) -> Void) -> Void {
  // Add code here to store the student object
  completion(student, nil)
}

router.post("/students", handler: saveStudents)

This has implemented the following URI: POST: /students which will receive a Student that needs to be persisted in the database.

Because the ORM and Kitura APIs have been aligned, storing the Student to the database only requires one line of code:

func saveStudents(student: Student, 
                  completion: @escaping (Student?, RequestError?) -> Void) -> Void {
  student.save(completion)
}

and because the format of the completion handlers are the same, you can pass the completion hander for the route directly into the ORM call.

Finding

The same approach can be used for each of the REST APIs. In order to create a Kitura route handler that returns all of the students in the database, the following is used:

func getStudents(completion: @escaping ([Student]?, RequestError?) -> Void) -> Void {
  Student.findAll(completion)
}

router.get("/students", handler: getStudents)

This has implemented the following URI: GET: /students which will receive all the students from the database.

Deleting

Deleting the students takes a similar approach, this time by registering with the Router for DELETE requests on /students:

func deleteStudents(completion: @escaping (RequestError) -> Void) -> Void {
  Student.deleteAll(completion)
}

router.delete("/students", handler: deleteStudents)

This has implemented the following URI: DELETE: /students which will delete all the students from the database.

The Future

In Kitura 2.2, we introduced Query Parameters, which are used to filter on data. The next step is to integrate this capability directly into the ORM to make it easy to filter the results:

The following shows how that might look, and how by aligning the APIs very limited additional code is required:

struct Student: Model {
  let name: String
  let course: String 
  let grade: Int
}

struct Query: QueryParams {
  let name: String
}

router.get("/students", hander: getStudents)

func getStudents(query: QueryParams, 
                 completion: @escaping ([Student]?, RequestError?) -> Void) -> Void {
  Student.findAll(query, completion)
}

We’re interested in your feedback on this potential API, and your ideas on how the ORM can develop in future.

Try It Out!

In order to make it easier to get hands on and try out the ORM, our FoodTrackerBackend tutorial has been updated to save and fetch the Meal objects from the iOS app to a PostgreSQL database using the ORM. Even if you’ve done the tutorial before, it’s well worth going through it again to see the power of Swift-Kuery-ORM and how it can simplify your Kitura code.






Join the discussion on Slack Learn more at kitura.io Star Kitura on GitHub

4 comments on"Introducing Swift-Kuery-ORM"

  1. How to update data with specific column?

  2. Hi Enrique Lacal,

    wow! It’s very convenient and useful for accessing database.
    Do you have any plan to access database using specific column in the future?
    ex:
    Grade.find(specific column: value) { … }
    Grade.update(specific column: value) { … }
    Grade.delete(specific column: value) { … }
    Thanks

Join The Discussion

Your email address will not be published. Required fields are marked *