Modeling in Tailor

Persisting Records

Tailor's modeling system is designed to allow you to model your domain objects in whatever way best suits your domain. Tailor provides a simple protocol for describing how a model relates to a database table, called Persistable. This protocol requires that you provide four things: An id field for holding the record ID, an initializer to create an instance from a row in the database, a function to get the columns and values to persist for an instance, and a class function providing the name for the table. Nothing about the protocol assumes that your models are represented by classes, so you can use structs for your models if you prefer.

The initializer will take in a DatabaseRow record, which contains a dictionary mapping column names to values from the database. These values will be wrapped in the DatabaseValue enum, which wraps around the raw values so that we can pass them around in a type-safe way. The DatabaseRow type provides a helper method for extracting the raw value safely. The read method on DatabaseRow takes in the name of a column in the database and returns the value for that column, cast to whatever type you want. It infers the cast type from the type of the variable you are trying to store it in. If the value is missing, or of the wrong type, the read method will throw an exception described in the DatabaseError enum. There is also a variant of the read method that returns an optional, and will not throw an error if the value is missing, but will return nil instead. If an error is thrown when building a record in response to a query, the system will log the error and skip the record, making the record effectively invisible to your app.

The valuesToPersist method must provide all the columns that are persisted for a record, besides the id method. This must return a dictionary mapping column names to DatabaseValuePersistable values. That protocol describes any type that can be used to create a DatabaseValue. These can be strings, numbers, dates, data blobs, or even custom types that implement the protocol. The values in the dictionary can also be nil, which will be translated into a null value in the database.

The persistance itself is handled by the save method. This saves a record to the database based on the mapping in the functions for the Persistable protocol. If the mapping has an entry for a created_at column, it will set it to the current time upon creation. If the mapping has an entry for an updated_at column, it will set it to the current time every time save is called. The return value from save is another instance of the same model type with the latest values. For instance, the returned record may have the created_at and updated_at columns set. For a record that is being created, it would also have the id column set. This method returns an optional, and will return nil only if there is an error running the query for saving the record.

Tailor also provides some helper functions for fetching related records. The toMany method returns a query for fetching records that have a foreign key pointing to that record. It has an optional parameter for the name of the foreign key in the other table, but if it is omitted the foreignKeyName method on the original type will be used to generate one. The type of the other table is inferred from context. For instance, if you call let orders: Query<Order> = hat.toMany(), it will return a query for entries in the orders table where the hat_id column is equal to that hat's id. This type inference can be particularly helpful when you're defining methods on your model types, as shown below.

There is also a variant of toMany that takes a query and builds another query for a different table that is joined to the entries in the first query. For instance, let's assume we have the orders query from the previous example, and that we want to get the customers that have placed the orders in that query. Calling let customers: Query<Customer> = hat.toMany(through: orders) would build a query for any customer who has placed an order for a hat with the id provided earlier. The SQL would look something like:

SELECT customers.* FROM customers INNER JOIN orders ON orders.customer_id =
customers.id WHERE orders.hat_id = 5

This function also has an optional parameter for the foreign key, and will infer it in the same way as the normal toMany method. It has another parameter called joinToMany which specifies the direction of the join. The default is false, which means that each record in the intermediary query is only giving us one record in the result set. In that case, there is a foreign key from the intermediary table pointing into the result table. If this is true, the relationship is reversed. Each record in the intermediary query can give us many records in the result table, and the foreign key points from the result table to the intermdiary table. If you call let customers: Query<Customer> = toManyRecords(through: orders, joinToMany: true), it will give you this SQL:

SELECT customers.* FROM customers INNER JOIN orders ON orders.id =
customers.order_id WHERE orders.hat_id = 5

There is also a method called destroy which deletes a record from the database based on its id. There is an equality operator defined for any type that implements the Persistable protocol, which compares the records based on their ids. There is also a foreignKeyName method which takes in a type and returns the name of a column that would be in another table containing the id of a record of that type. For instance, Hat.foreignKeyName() would return "hats".

Record Example

Here's an example of what an app's model class might look like:

// This class models a hat in a store.
struct Hat: Persistable {

  // MARK: - Structure

  // The primary key of the record.
  var id: Int?

  // The color of the hat.
  var color: String

  // The size of the hat's brim.
  var brimSize: Int

  // The id of the store this hat is sold in.
  var storeId: Int

  // The time when the hat was created.
  let createdAt: Timestamp?

  // The time when the hat was last updated.
  let updatedAt: Timestamp? 

  init(color: String, brimSize: Int, storeId: Int) {
    self.color = color
    self.brimSize = brimSize
    self.storeId = storeId
    self.id = nil
    self.createdAt = nil
    self.updatedAt = nil
  }

  //MARK: - Associations

  // This method gets the store that this hat is sold in.
  var store: Store? { return Query<Store>().find(storeId) }

  // This method gets the orders that were made for this hat.
  //
  // It returns a Query rather than an Array so that you can run other
  // filters or related queries on this relationship without having to fetch
  // all the records from the database.
  var orders: Query<Order> { return toMany() }

  // This methods gets the customers who have purchased this hat.
  var customers: Query<Customer> { return toMany(through: orders) }

  //MARK: - Persistence

  static var tableName: String { return "hats" }

  init(databaseRow: DatabaseRow) throws {
    self.id = try databaseRow.read("id")
    self.color = try databaseRow.read("color")
    self.brimSize = try databaseRow.read("brim_size")
    self.storeId = try databaseRow.read("store_id")
    self.createdAt = try databaseRow.read("created_at")
    self.updatedAt = try databaseRow.read("updated_at")
  }

  func valuesToPersist() -> [String:DatabaseValueConvertible?] {
    return [
      "color": color,
      "brim_size": brimSize,
      "store_id": storeId,
      "created_at": createdAt,
      "updated_at": updatedAt
    ]
  }

  static let query = Query<Hat>()
}

Validators and Errors

A Validation runs checks on a model object and collects errors when those checks fail. Each check returns a new validation with that new error applied, so you can chain the calls together. Most checks will require both the name of a field and a field value. The name of the field is used to generate a human readable error message, and the value is used to do the actual validation.

The validate(presenceOf) method checks that a value is present. The validate(inBounds) method checks that a value is in a certain range. The validate(uniquenessOf) method checks that a set of fields have not already been used for a record of that model type.

If you want to do custom validations, you can extend the Validation struct with your own validation method, use the validate method that takes in a block, or just call the withError method to manually add errors to the validation.

The validation's errors field will hold all the errors. Each of these will be a ValidationError. These errors have a modelName, a key, and a message, which are combined to form a key for a localized error message. The guide on views will have more details about localization. For an error on the color key on the hat model, with the message blank, the key for the localized message would be hat.errors.color.blank. If it cannot find a message for that key, it will fall back to hat.errors.blank, model.errors.color.blank, and model.errors.blank. Validation errors also have a data dictionary with additional details about the error. These will be interpolated into the final error message.

If you wanted to run several checks on a hat record before saving it, that might look like this:

let validation = Validation("hat")
  .validate(presenceOf: "color", hat.color)
  .validate("brimSize", hat.brimSize, inRange: 5...15)

if validation.valid {
  saveRecord(hat)
}

Query Building

The Query type provides a way to build and execute queries. The query-building interface parallels the structure of a MySQL query. The class is parameterized with the type of record that the query is fetching. To create an empty query for fetching hats, you would call Query<Hat>(). That will build a query that, when executed, will fetch all the records from the hats table The class has a flexible initializer that can take any part of the query, as well as another query to copy its components from. Those are mostly useful internally, though. You can build queries by starting with an empty query and call methods on it to build out the rest of the query. All of these query building methods return a new query, and leave the original unmodified. This means that you can chain them together or store intermediate results in local variables.

The filter method constructs the "where" clause of the query, defining which rows should be returned in the result set. This method has two forms. The first takes the raw SQL for the where clause, and the values to use as bind parameters for the query. The second form takes a dictionary of attributes to look for in the result set. The keys for these attributes should be the names of columns in the database.

The join method makes the query join to another table. It has two forms. The first form takes the raw SQL for the join clause and the bind parameters for that part of the query. This form needs to include the join keyword itself, so that we can support both inner and outer joins. The second form takes another record type, the name of a field on the target record type, and the name of a field on the main record type. This form allows you to work with the way your models are represented in code rather than in SQL.

There are a few more methods that map closely to portions of the SQL statement. The order method constructs the "order" clause of the query, taking in the name of a field on the model and an ordering. The order method can be chained multiple times, and will apply all the orderings, giving precedence to the ones that are added first. The limit method specifies the maximum number of results to return. The reverse method reverses the ordering on a query. The select method specifies the fields that should be fetched in the result set. The field names for the select method need to be the database column names, and the parameter needs to be formatted as a SQL select statement.

The Query class also has several methods for fetching the results of a query. the toSql method combines the portions of the query into a single SQL statement. The all method executes the query and builds the records with those results. The first method returns just the first result from a query, and the last method returns just the last. Both of those methods put a limit statement in the SQL, so they can be run efficiently even if the full result set of the query is too large. The find method fetches a record by id, but it also applies any other filters on the query. If there are no results for a first, last, or find call, they return nil. The count method returns the number if results for a query. The count is calculated on the database server. The isEmpty method determines if a query has no results.

The Query type implements a protocol called QueryType, which provides much of the actual query building logic. There's another type called GenericQuery that implements the same protocol, but that takes its type from parameters to the initializer rather than a parameter on the type itself. This is mainly to support having a default implementation for the query method on the Persistable protocol; that method is typed as returning a QueryType value, and the default implementation returns a GenericQuery. You will generally want to override this to provide a more typed version. The Query type also provides more methods for fetching: The GenericQuery protocol just provides allRecords, which returns an array of Persistable values; the Query type provides an all method which returns an array of values typed to the specific type for the query, as well as all of the other methods in the previous paragraph.

Query Examples

var query = Query<Hat>().filter(["color": "red"]).filter("brim_size > ?", ["10"])
query = query.order("brim_size", .OrderedAscending)

// Will execute SELECT * FROM `hats` WHERE `color`="red" AND `brim_size` >
// 10 ORDER BY `brim_size` ASC
let hats = query.all()

for hat in hats {
  print(String(hats.id!))
}

// Will execute SELECT * FROM `hats` WHERE `color`="red" AND `brim_size` >
// 10 ORDER BY `brim_size` DESC LIMIT 1

if let hat = query.last() {
  print("Last hat is \(hat.id)")
}
else {
  print("No hat")
}