-
Notifications
You must be signed in to change notification settings - Fork 13
Models
SQL has ORM-style functionality, focusing on efficiency, transparency and safety. Your specific connector should have a Model
protocol, that extends SQL.Model
. Let's go through how to implement it.
Create your basic model struct. Using structs are good practice, as SQL
modifies your object when saving and updating by replacing the entire struct. Using a class is not recommended.
import PostgreSQL
struct Artist {
let id: Int?
// Note the optionality. Fields that are marked `NOT NULL` in your schema should be non-optional.
var name: String
var genre: String?
// You probably want your own initializer
init(name: String, genre: String) {
self.id = nil
self.name = name
self.genre = genre
}
}
In order to conform to the Model
protocol, we have to extend Artist
. You can put this code directly in your struct if you like.
extension Artist: Model {
// Define the fields of your model.
// Make sure to name it `Field` and have it conform to `String` and `FieldType`
enum Field: String, FieldType {
case Id = "id"
case Name = "name"
case Genre = "genre"
}
// Table name corresponding to your model
static let tableName: String = "artists"
// The field for the primary key of the model
static let fieldForPrimaryKey: Field = .Id
// The fields used when constructing your model.
// You don't have to match all the values in your `Field` enum, unless you want to.
// NOTE: Leave this out to select all fields.
static let selectFields: [Field] = [
.Id,
.Name,
.Genre
]
// The model needs to know the value of your primary key.
// You can name the actual variable of your struct representing the primary key
// to `primaryKey`, or provide it like shown below.
// NOTE: You can use any type as your primary key, as long as it conforms to `SQLDataConvertible`
var primaryKey: Int? {
return id
}
// Add the ability to construct the model from a row
init(row: Row) throws {
id = try row.value(Artist.field(.Id))
name = try row.value(Artist.field(.Name))
genre = try row.value(Artist.field(.Genre))
}
// Define which values are allowed to be persisted.
// Note whether your properties are mutable.
var persistedValuesByField: [Field: SQLDataConvertible?] {
return [
.Name: name,
.Genre: genre
]
}
}
While this solution is generally more verbose than most ORM's, there's no magic, it's very clear what's going on.
Let's also create an Album
model.
struct Album {
struct Error: ErrorType {
let description: String
}
let id: Int?
var name: String
var artistId: Int
init(name: String, artist: Artist) throws {
guard let artistId = artist.id else {
throw Error(description: "Artist doesn't have an id yet")
}
self.name = name
self.artistId = artistId
self.id = nil
}
}
extension Album: Model {
enum Field: String, FieldType {
case Id = "id"
case Name = "name"
case ArtistId = "artist_id"
}
static let tableName: String = "albums"
static let fieldForPrimaryKey: Field = .Id
static let selectFields: [Field] = [
.Id,
.Name,
.ArtistId
]
var primaryKey: Int? {
return id
}
init(row: Row) throws {
id = try row.value(Album.field(.Id))
name = try row.value(Album.field(.Name))
artistId = try row.value(Album.field(.ArtistId))
}
var persistedValuesByField: [Field: SQLDataConvertible?] {
return [
.Name: name,
.ArtistId: artistId
]
}
}
After creating our models, we can use a more safe way of creating queries.
Artist.selectQuery.join(Album.self, type: .Inner, leftKey: .Id, rightKey: .ArtistId)
Artist.selectQuery.limit(10).offset(1)
Artist.selectQuery.orderBy(.Descending(.Name), .Ascending(.Id))
When working with model queries, you use Model.field()
, specifying your models Field
enum to get the declared fields.
Artist.selectQuery.filter(Artist.field(.Id) == 1 || Artist.field(.Genre) == "rock")
Using model selects, you can call fetch
and first
let artists = try Artist.selectQuery.fetch(connection) // [Artist]
let artist = try Artist.selectQuery.first(connection) // Artist?
let artist = try Artist.get(1, connection: connection) // Artist?
You can insert new models either by simply providing a dictionary with fields and values
let artist = try Artist.create([.Name: "AC/DC", .Genre: "rock"], connection: connection)
Or, you can construct a model using your own initializers
var artist = Artist(name: "Kendrick Lamar", genre: "hip hop")
try artist.create(connection)
You can simply call save
var artist = Artist(name: "Hocus pocus", genre: "hip hop")
try artist.save(connection)
save
will either call insert
or update
depending on whether the model has a primary key.
try artist.delete(connection: connection)
Add a method with the following signature to your models. Throw an error if validation failed. Not throwing an error indicates that the validation passed.
Validations are called before willSave()
, willUpdate()
, and willCreate()
func validate()throws {
guard name == "david" else {
throw MyError("Name has to be 'david'")
}
}
You can use any of the following hooks in your model.
-
willSave
&didSave
-
willUpdate
&didUpdate
-
willCreate
&didCreate
-
willDelete
&didDelete
-
willRefresh
&didRefresh
extension Artist {
func willSave() {
}
func didSave() {
}
}
For updates the flow is: willSave() -> willUpdate -> (UPDATE) -> didUpdate() -> willRefresh() -> (REFRESH) -> didRefresh() -> didSave()
For creates, the flow is similar, except the updates
are creates
.