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()