Welcome to Amigo!¶
A SQLite ORM for Swift 2.1+ powered by FMDB
But What About CoreData?¶
Yup, CoreData...
Requirements¶
- Swift 2.1 (aka iOS 9+, os x 10.11+)
- CoreData
The Deal¶
So, we like CoreData, but we also like ORMs in the flavor of SQLAlchemy or Django’s ORM. So lets make that happen, however unwisely, in Swift. To that end you could say at this point that Amigo was firstly inspired by the version of Amigo we wrote for C#/Xamarin which you can find here:
https://github.com/blitzagency/amigo
But what if we just want to work in Swift you say? Well, lets rewrite Amigo in Swift then! So we did. It’s a start. A start that shamelessly copies, probably poorly, ideas found in SQLAlchemy and Django.
Contents:
Setup¶
Carthage¶
FMDB (https://github.com/ccgus/fmdb), as of v 2.6 FMDB, does suport Carthage.
Drop this into your Cartfile
:
github "blitzagency/amigo-swift" ~> 0.3.1
Admittedly, we are probably not the best at the whole, “How do you share an Xcode Project” thing, so any recommendations to imporve this process are welcome.
Initialization Using A Closure¶
Initialize Amigo into a global variable so all the initialization is done in one place:
import Amigo class Dog: AmigoModel{ dynamic var id = 0 dynamic var label = "" } let amigo: Amigo = { let dog = ORMModel(Dog.self, IntegerField("id", primaryKey: true), CharField("label") ) // now initialize Amigo // specifying 'echo: true' will have amigo print out // all of the SQL commands it's generating. let engine = SQLiteEngineFactory(":memory:", echo: true) let amigo = Amigo([dog], factory: engine) amigo.createAll() return amigo }()
Note
This creates the amigo
object lazily, which means it’s not
created until it’s actually needed. This delays the initial
output of the app information details. Because of this,
we recommend forcing the amigo
object to be created
at app launch by just referencing amigo
at the top of
your didFinishLaunching
method if you don’t
already use the amigo
object for something on app launch.
This style and description was taken directly from [XCGLogger]
A Note About Threads and Async¶
Out of the box, Amigo uses FMDB’s FMDatabaseQueue
to perform all
of it work. This should set you up to user Amigo from any thread you like.
Keep in mind though, the dispatch queue that FMDatabasesQueue
uses
is a serial queue. So if you invoke multiple long running amigo commands
from separate threads you will be waiting your turn in the serial queue
before your command is executed. It’s probably best to:
func doStuff(callback: ([YourModelType]) -> ()){
let session = amigo.session
// any background queue you like
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
disptatch_async(queue) {
let results = session.query(YourModelType).all() // whatever amigo command you want
dispatch_async(dispatch_get_main_queue()){
callback(results)
}
}
}
If that’s too verbose for your liking you are also welcome to use the
async
convenience methods provided by the amigo session. The
above code would then look like this:
func doStuff(callback: ([YourModelType]) -> ()){
let session = amigo.session
session.async{ () -> [YourModelType] in
let results = session.query(YourModelType).all()
return results
}.then(callback)
}
There are a few ways to use these async handlers. The variations revolve around weather or not you are returning results. Check out the unit tests [AmigoSessionAsyncTests] for more examples.
For example, you don’t have to return any result at all:
func addObject(){
let session = amigo.session
session.async{
let dog = Dog()
dog.label = "Lucy"
session.add(dog)
}
}
func addBatch(){
let session = amigo.session
session.async{
let d1 = Dog()
d1.label = "Lucy"
let d2 = Dog()
d2.label = "Ollie"
session.batch{ batch in
batch.add([d1, d2])
}
}
}
You can also specify your own background queue to execute on:
let queue = dispatch_queue_create("com.amigo.async.tests", nil)
func addDoStuffOnMyOwnQueue(){
let session = amigo.session
session.async(queue: queue){
let dog = Dog()
dog.label = "Lucy"
session.add(dog)
}
}
Contents:
Engine Setup¶
An instance of Amigo needs an engine to run the queries. Amigo comes with a SQLiteEngine out of the box. Let’s take a look at setting it up so we can create our schema:
import Amigo
import CoreData
// the first arg can be ":memory:" for an in-memory
// database, or it can be the absolute path to your
// sqlite database.
//
// echo : Boolean
// true prints out the SQL statements with params
// the default value of false does nothing.
let mom = NSManagedObjectModel(contentsOfURL: url)!
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo(mom, factory: engine)
amigo.createAll()
Note, in the example above Amigo can process a ManagedObjectModel
to create it’s schema. See models/mom
for more.
[XCGLogger] | XCGLogger Closure Initialization https://github.com/DaveWoodCom/XCGLogger#initialization-using-a-closure |
[AmigoSessionAsyncTests] | AmigoSessionAsyncTests https://github.com/blitzagency/amigo-swift/blob/master/AmigoTests/AmigoSessionAsyncTests.swift |
Models¶
Amigo works by mapping your data models derived from a
Amigo.AmigoModel
into an Amigo.ORMModel
There are 2 way you can perform this mapping.
- Use a Managed Object Model
- Manual Mapping
The Managed Object Model mapping just automates the Manual Mapping process.
Contents:
Managed Object Model¶
Yup, Amigo can turn NSEntityDescriptions
along with their
relationships into your tables for you. There are only a couple things to know.
- Unlike
CoreData
, you need to specify your primary key field. This could totally be automated for you, we havent decided if we like that or not yet. You do this by picking your attribute in your entity and adding the following to the User Info: primaryKey true. Crack open theApp.xcdatamodeld
and look at any of the entities for more info. - You need to be sure the Class you assign to the entity in your xcdatamodeld is a subclass of AmigoModel
You do not have to use a NSManagedObjectModel
at all. In fact,
we just use it to convert the NSEntityDescriptions
into
:code: ORMModel instances. It’s fine if you want to do the mappings
yourself. See ORM Model Mapping for more.
ORM Model Mapping¶
Amigo can parse a NSManagedObjectModel
but all it’s doing is
converting the NSEntityDescriptions
into ORMModel
instances. Lets take a look at how we do that.
Important
When performing a model mapping your data models MUST
inherit from Amigo.AmigoModel
import Amigo
class Dog: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
Index("dog_label_idx", "label")
)
// you could achieve the same mapping this way:
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self, indexed: true)
)
// now initialize Amigo
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo([dog], factory: engine)
amigo.createAll()
Column Options¶
Columns can be initialized with the following options (default values presented):
type: // See Column Types below
primaryKey: Bool = false
indexed: Bool = false
optional: Bool = true
unique: Bool = false
defaultValue: (() -> AnyObject?)? = nil
Column Types¶
Your avavilable options for Column types are as follows:
NSString
String
Int16
Int32
Int64
Int
NSDate
NSData
[UInt8]
NSDecimalNumber
Double
Float
Bool
These effectvely map to the following NSAttributeType
found in CoreData
which you may also use for your column initialization:
NSAttributeType.StringAttributeType
NSAttributeType.Integer16AttributeType
NSAttributeType.Integer32AttributeType
NSAttributeType.Integer64AttributeType
NSAttributeType.DateAttributeType
NSAttributeType.BinaryDataAttributeType
NSAttributeType.DecimalAttributeType
NSAttributeType.DoubleAttributeType
NSAttributeType.FloatAttributeType
NSAttributeType.BooleanAttributeType
NSAttributeType.UndefinedAttributeType
See the initializers in:
https://github.com/blitzagency/amigo-swift/blob/master/Amigo/Column.swift
Column Shortcuts (Fields)¶
Amigo also provides a series pre-baked column types.
https://github.com/blitzagency/amigo-swift/blob/master/Amigo/Fields.swift
They take the same arguments as a Column
, but the type can be omitted.
UUIDField
CharField
BooleanField
IntegerField
FloatField
DoubleField
BinaryField
DateTimeField // needs some work
Note
The UUIDField, in SQLite, will store your data as a 16 byte BLOB
but it on the model itself it will be realized as a String
.
Lets take a look at how using these might look:
class MyModel: AmigoModel{
dynamic var id: Int = 0
dynamic var objId: String?
public override init(){
super.init()
}
}
let myModel = ORMModel(MyModel.self,
IntegerField("id", primaryKey: true),
UUIDField("objId", indexed: true, unique: true)
)
// now initialize Amigo
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
let amigo = Amigo([myModel], factory: engine)
amigo.createAll()
let objId = NSUUID().UUIDString
let obj = MyModel()
obj.objId = objId
let session = amigo.session
session.add(obj)
let results = session
.query(MyModel)
.filter("objId = '\(objId)'")
.all()
Remember above how we said that the UUIDField
stores it’s data
as a BLOB. The above filter does the right thing, it will convert the
privided string when filtering on the UUIDField
to it’s data
representation.
UUIDField¶
The UUIDField will serve you best if you provide a defaultValue
function. 99% of the time you will likely want something like this
when you define the column unless you want to be responsible for always
setting the value yourself:
import Amigo
class MyModel: AmigoModel{
dynamic var id: Int = 0
dynamic var objId: String?
public override init(){
super.init()
}
}
let myModel = ORMModel(MyModel.self,
IntegerField("id", primaryKey: true)
UUIDField("objId", indexed: true, unique: true){
// the input case doesn't actually matter, but
// rfc 4122 states that:
//
// The hexadecimal values "a" through "f" are output as
// lower case characters and are case insensitive on input.
//
// See: https://www.ietf.org/rfc/rfc4122.txt
// Declaration of syntactic structure
return NSUUID().UUIDString.lowercaseString
}
)
Note
The deserialization of the UUIDField will return you a String that is lowercased per RFC 4122.
One additional type exists for Column initialization and that’s Amigo.ForeignKey
ForeignKeys¶
Amigo allows you to make Foreign Key Relationships. You can do though through the Managed Object Model or manually.
In the Managed Object Model, ForeignKeys are represented by a Relationship
that has a type of To One
. That gets translated to the ORMModel
mapping as follows:
import Amigo
class Dog: AmigoModel{
dynamic var id = 0
dynamic var label = "New Dog"
public override init(){
super.init()
}
}
class Person: AmigoModel{
dynamic var id = 0
dynamic var label = "New Person"
dynamic var dog: Dog?
public override init(){
super.init()
}
}
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
)
// You can use the ORMModel
let person = ORMModel(Person.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
Column("dog", type: ForeignKey(dog))
)
or using the column itself
// OR you can use the column:
let person = ORMModel(Person.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
Column("dog", type: ForeignKey(dog.table.c["id"]))
)
One To Many¶
Using our Person/Dog
example above, we can also represent a
One To Many relationship.
In the case of a Managed Object Model, a One To Many is represented by a
Relationship that has a type on To One
on one side and
To Many
on the other side, aka the inverse relationship.
In code it would look like this:
import Amigo
class Dog: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
class Person: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
dynamic var dog: Dog!
public override init(){
super.init()
}
}
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
OneToMany("people", using: Person.self)
)
let person = ORMModel(Person.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
Column("dog", type: ForeignKey(dog))
)
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo([dog, person], factory: engine)
amigo.createAll()
We can then query the One To Many Relationship this way:
let session = amigo.session
let d1 = Dog()
d1.label = "Lucy"
let p1 = People()
p1.label = "Foo"
p1.dog = d1
let p2 = People()
p2.label = "Bar"
p2.dog = d1
session.add(d1, p1, p2)
var results = session
.query(People) // We want the People objects
.using(d1) // by using the d1 (Dog) object
.relationship("people") // and following the d1 model's "people" relationship
.all()
Many To Many¶
Amigo can also represent Many To Many Relationships. It will build the intermediate table for you as well.
In the case of a Managed Object Model, a Many To Many is represented by a
Relationship that has a type on To Many
on one side and
To Many
on the other side, aka the inverse relationship.
Starting with the following data models:
import Amigo
// ---- Many To Many ----
// A Parent can have Many Children
// and children can have Many Parents
class Parent: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
class Child: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
Now, lets manually map them and create the relationship:
let parent = ORMModel(Parent.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
ManyToMany("children", using: Child.self)
)
let child = ORMModel(Child.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
)
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo([parent, child], factory: engine)
amigo.createAll()
let session = amigo.session
let p1 = Parent()
p1.label = "Foo"
let c1 = Child()
c1.label = "Baz"
let c2 = Child()
c2.label = "Qux"
session.add(p1, c1, c2)
// add 2 children to p1
session.using(p1).relationship("children").add(c1, c2)
var results = session
.query(Child) // We want the Child objects
.using(p1) // by using the p1 (Parent) object
.relationship("children") // and following the p1 model's "children" relationship
.all()
print(results.count)
Extra Fields on Many To Many Relationships¶
Sometimes you need more information on a Many To Many Relationship than just the 2 original models. We have shamelessly taken this concept from Django and matched their name: “Though” Models.
In the case of a Managed Object Model, a Many To Many with a “Through” models
is represented by a Relationship that has a type on To Many
on one side and
To Many
on the other side, aka the inverse relationship. Additionally,
the User Info
of the relationship has the following key value pair:
throughModel = Fully Qualified AmigoModel Subclass Name
Lets make a manual example.
import Amigo
// ---- Many To Many (through model) ----
// A Workout can have Many Exercises
// An exercise can belong to Many Workouts
// We attach some extra Meta information to
// the relationship though.
class Workout: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
class WorkoutExercise: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
public override init(){
super.init()
}
}
class WorkoutMeta: AmigoModel{
dynamic var id: NSNumber!
dynamic var duration: NSNumber!
dynamic var position: NSNumber!
dynamic var exercise: WorkoutExercise!
dynamic var workout: Workout!
}
Now, lets manually map them and create the relationship:
let workout = ORMModel(Workout.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
ManyToMany("exercises", using: WorkoutExercise.self, throughModel: WorkoutMeta.self)
)
let workoutExercise = ORMModel(WorkoutExercise.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
)
let workoutMeta = ORMModel(WorkoutMeta.self,
Column("id", type: Int.self, primaryKey: true),
Column("duration", type: Int.self),
Column("position", type: Int.self),
Column("exercise", type: ForeignKey(workoutExercise)),
Column("workout", type: ForeignKey(workout))
)
Note
Look at the mapping for WorkoutMeta
. If you are going to
use a throughModel
the model that will we will go though
MUST contain 2 ForeignKey
columns. They MUST map to
the 2 columns that are required for the many-to-many relationship.
Now that we are mapped, lets try adding an exercise without using the WorkoutMeta.
let session = amigo.session
let w1 = Workout()
w1.label = "foo"
let e1 = WorkoutExercise()
e1.label = "Jumping Jacks"
session.add(w1)
session.add(e1)
// This will cause a fatal error.
session.using(w1).relationship("exercises").add(e1)
Because we have instructed Amigo that this many-to-many relationship
uses a “through” model, we can no longer use the many-to-many add or delete
functionality, as the WorkoutMeta
model is required.
Instead, you simply add a WorkoutMeta
model like any other model.
Amigo handles the insert into the intermediate table for you.
let session = amigo.session
let w1 = Workout()
w1.label = "foo"
let e1 = WorkoutExercise()
e1.label = "Jumping Jacks"
let m1 = WorkoutMeta()
m1.workout = w1
m1.exercise = e1
m1.duration = 60000
m1.position = 1
session.add(w1, e1, m1)
// querying the many-to-many however is the same.
var results = session
.query(WorkoutMeta) // We want the WorkoutMeta objects
.using(w1) // by using the w1 (Workout) object
.relationship("exercises") // and following the w1 model's "exercises" relationship
.orderBy("position", ascending: true) // order the results by WorkoutMeta.position ascending
.all()
Querying¶
Querying with Amigo
is similar to querying with Django or
SQLAlchemy. Lets run though a few examples. In each of the following
examples we will assume we have already done our model mapping
and we have an amigo
instance available to us. For
more information on model mapping see: Models
Get an object by id¶
get
returns an optional model as it may fail if the id
is not present.
let session = amigo.session
let maybeDog: Dog? = session.query(Dog).get(1)
Order objects¶
orderBy
let session = amigo.session
let dogs: [Dog] = session
.query(Dog)
.orderBy("label", ascending: false) // by default ascending is true
.all()
Filter objects¶
filter
let session = amigo.session
let dogs: [Dog] = session
.query(Dog)
.filter("id > 3")
.all()
Note
Filter strings are converted into a NSPredicate
behind the
scenes. When using the SQLiteEngine
, the constant params are
extracted and replaced with ? in generated query. The params are
then passed to FMDB for escaping/replacement.
Limit objects¶
limit
let session = amigo.session
let dogs: [Dog] = session
.query(Dog)
.limit(10)
.all()
Offset objects¶
offset
let session = amigo.session
let dogs: [Dog] = session
.query(Dog)
.limit(10)
.offset(5)
.all()
Full foreign key in one query (aka JOIN)¶
selectRelated
See ForeignKeys for more.
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
)
// You can use the ORMModel
let person = ORMModel(Person.self,
Column("id", type: Int.self, primaryKey: true)
Column("label", type: String.self)
Column("dog", type: ForeignKey(dog))
)
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo([dog, person], factory: engine)
amigo.createAll()
let session = amigo.session
let d1 = Dog()
d1.label = "Lucy"
let p1 = Person()
p1.label = "Foo"
p1.dog = d1
session.add(d1, p1)
let result = session
.query(Person)
.selectRelated("dog")
.all()
One-To-Many Query¶
relationship
See One To Many for the full example.
let session = amigo.session
var results = session
.query(People) // We want the People objects
.using(d1) // by using the d1 (Dog) object
.relationship("people") // and following the d1 model's "people" relationship
.all()
Many-To-Many Query¶
relationship
See Many To Many for the full example.
let session = amigo.session
var results = session
.query(Child) // We want the Child objects
.using(p1) // by using the p1 (Parent) object
.relationship("children") // and following the d1 model's "children" relationship
.all()
Many-To-Many With Through Models Query¶
relationship
See Extra Fields on Many To Many Relationships for the full example.
let session = amigo.session
var results = session
.query(WorkoutMeta) // We want the WorkoutMeta objects
.using(w1) // by using the w1 (Workout) object
.relationship("exercises") // and following the w1 model's "exercises" relationship
.orderBy("position", ascending: true) // order the results by WorkoutMeta.position ascending
.all()
Sessions¶
When you go though amigo.session
using the provided
SQLiteEngine
you automatically begin a SQL Transaction.
If you would like your information to actually be persisted you must
commit
the transaction. Once committed, the session will
automatically begin a new transaciton for you.
import Amigo
class Dog: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
}
class Person: AmigoModel{
dynamic var id: NSNumber!
dynamic var label: String!
dynamic var dog: Dog!
}
let dog = ORMModel(Dog.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
OneToMany("people", using: Person.self, on: "dog")
)
let person = ORMModel(Person.self,
Column("id", type: Int.self, primaryKey: true),
Column("label", type: String.self),
Column("dog", type: ForeignKey(dog))
)
// specifying 'echo: true' will have amigo print out
// all of the SQL commands it's generating.
let engine = SQLiteEngineFactory(":memory:", echo: true)
amigo = Amigo([dog, person], factory: engine)
amigo.createAll()
let session = amigo.session
let d1 = Dog()
let d2 = Dog()
d1.label = "Lucy"
d2.label = "Ollie"
session.add(d1, d2)
session.commit()
Upsert¶
When inserting a model, you have the option to choose weather or not
you would like this to be done as an insert or an upsert. In sqlite
this translates to INSERT OR REPLACE
. To take advantage of this
you need to pass an additional argument to session.add
.
session.add(myModel, upsert: true)
Batching¶
If you would like to batch a significant number of queries Amigo supports this for add and delete.
let session = amigo.session
session.batch{ bacth in
myAdds.forEach(batch.add)
myDeletes.forEach(batch.add)
myUpserts.forEach{ batch.add($0, upsert: true) }
}
This will take all of the generated sql and execute at once. It’s a
convenience over FMDB executeStatements
https://github.com/ccgus/fmdb#multiple-statements-and-batch-stuff
Important
When you use this functionality Amigo does not make any
updates to the source models. For example, doing an
session.add
will modify the source model with
the primary key assigned to it by immediately issuing a
SELECT last_insert_rowid();
. However, batch.add
will not do this.
Warning
If you use batching with Many-To-Many + Through Models you should
have all of the information necessary in advance for the
relationship. It’s not required, but if you don’t have all of the
Foreign Keys and Primary Key for the record, Amigo will skip batching
those items in favor of a regular session.add
to ensure
it has the proper information.