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.

  1. Use a Managed Object Model
  2. 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.

  1. 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 the App.xcdatamodeld and look at any of the entities for more info.
  2. 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)

Get all objects

all

let session = amigo.session
let dogs: [Dog] = session.query(Dog).all()

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.

Indices and tables