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 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. This enum provides helper methods for extracting the raw value safely. The initializer can be failable, and you are encouraged to return nil if the column does not have enough data to create a record. You can also have default values for when things are missing in the database; it depends on what is most appropriate for your domain.

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 saveRecord function. 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 saveRecord is called. The return value from saveRecord 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 toManyRecords function takes in a record and 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 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> = toManyRecords(hat), 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 toManyRecords 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> = toManyRecords(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 toManyRecords 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 function called destroyRecord 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, foreignKeyName(Hat.self) 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: NSDate?

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

  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 toManyRecords(self) }

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

  //MARK: - Persistence

  static var tableName: String { return "hats" }

  init?(databaseRow: [String:DatabaseValue]) {
    // This will be nil if there is no id column, if its value is itself
    // null, or if it is not an integer type.
    if let id = databaseRow["id"]?.intValue {
      self.id = id
      self.color = databaseRow["color"]?.stringValue ?? ""
      self.brimSize = databaseRow["brim_size"]?.intValue ?? 0
      self.storeId = databaseRow["store_id"]?.intValue ?? 0
      self.createdAt = databaseRow["created_at"]?.dateValue
      self.updatedAt = databaseRow["updated_at"]?.dateValue
    }
    else {
      // For some silly reason, you have to initialize all the fields in
      // an initializer before you can return nil. The values don't really
      // matter, though.
      self.id = nil
      self.color = ""
      self.brimSize = 0
      self.storeId = 0
      self.createdAt = nil
      self.updatedAt = nil
      return nil
    }

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

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,vuse 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 class 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.

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")
}