Simpler Core Data with RubyMotion and CDQ

Core Data can be one of the more intimidating Apple frameworks to work with, in part because it doesn’t quite have the polish that many of the other frameworks have but also because when you’re dealing with users’ data it is worth treading carefully: imagining correcting a poorly-executed migration on thousands of remote devices should be enough to persuade anyone of that!

Luckily, RubyMotion and Core Data Query can make Core Data a lot more familiar and manageable, particularly if you are already a Ruby/Rails developer. Core Data Query (or CDQ from here on out) is one of the ‘magic’ Core Data helper libraries that aims to abstract away much of the gnarlier parts of working with the framework, and something that builds naturally on top of ruby-xcdm (also from the author of CDQ) which makes the creation xcdatamodel files painless and more like ActiveRecord.

In this post I’ll take you through incorporating CDQ into a new RubyMotion project where we’ll build the beginnings of a Core Data powered task management app.

Getting Started

We start by creating a fresh project:

$ motion create learn-cdq
    Create learn-cdq
    Create learn-cdq/.gitignore
    Create learn-cdq/app/app_delegate.rb
    Create learn-cdq/Gemfile
    Create learn-cdq/Rakefile
    Create learn-cdq/resources/Default-568h@2x.png
    Create learn-cdq/spec/main_spec.rb

And make sure to load the Core Data framework by modifying our Rakefile:

Motion::Project::App.setup do |app|
  # Use `rake config' to see complete project settings.
  app.name = 'learn-cdq'
  app.frameworks += [ 'CoreData' ]
end

We’ll be needing the ‘cdq’ gem so we add gem 'cdq' to our Gemfile and run bundle install in the shell:

$ echo "gem 'cdq'" >> Gemfile
$ bundle install
Fetching gem metadata from https://rubygems.org/............
Resolving dependencies...
...
Installing cdq (0.1.10) 

Core Data Query requires an xcdatamodeld bundle to function. We can create one in XCode or use one from an existing project but we will use the ruby-xcdm gem to generate one. This is already included as a dependency of cdq.

We’re developing the bare bones of a task tracking app, so we start by outlining the basic model, which we put in schemas/001_initial.rb (you will need to create this directory):

schema "001" do
  entity "Task" do
    string :task_description, optional: false
  end
end

Aside: Erring on the side of caution

A word on model definitions, it is perhaps better to err on the side of having more fields rather than fewer, even if you don’t use them and may never use them, but you think you might. The reason for this is that it would be better to avoid migrations if at all possible, so if you think you might need a due date, a priority field or anything else then there is arguably little harm in putting it in there from the start. This is largely a matter of opinion though, if you would rather have a ‘cleaner’ model at the risk of having more migrations potentially then so long as the migrations are ‘light’, e.g. schema only then it should not have a noticeable impact on your apps in production.

Before continuing we build our schema:

$ rake schema:build
Generating Data Model learn-cdq
   Loading schemas/001_initial.rb
   Writing resources/learn-cdq.xcdatamodeld/001.xcdatamodel/contents

And use CDQ’s code generation feature to generate the model file for our Task model:

$ cdq create model task

     Creating model: task

  Δ  Creating directory: app/models
  Δ  Creating file: learn-cdq/app/models/task.rb
  Δ  Creating directory: spec/models
  Δ  Creating file: learn-cdq/spec/models/task.rb

     Done

At this stage the model file task.rb is largely empty:

class Task < CDQManagedObject

end

The final thing that we need to before we can start storing data is to set up the Core Data Query stack in the app delegate. Edit app/app_delegate.rb so that it matches the following:

class AppDelegate
  include CDQ

  def application(application, didFinishLaunchingWithOptions:launchOptions)
    cdq.setup
    true
  end
end

All that is required is to include CDQ and run cdq.setup when the application launches. This should handle for us the loading of the schema, persistent store (SQLite database) and the managed object context that we will work with.

Let’s run the app and start persisting some records:

(main)> Task.count
=> 0
(main)> t1 = Task.create(task_description: "Complete chapter")
=> <Task: 0x8f93950> (entity: Task; id: 0x8f93980 <x-coredata:///Task/t3C67EA36-48F6-4E0D-BA1D-9393664113FA2> ; data: {
    "task_description" = "Complete chapter";
})
(main)> t2 = Task.create(task_description: "File tax return")
=> <Task: 0x8f95300> (entity: Task; id: 0x8f95330 <x-coredata:///Task/t3C67EA36-48F6-4E0D-BA1D-9393664113FA3> ; data: {
    "task_description" = "File tax return";
})
(main)> cdq.save
=> true
(main)> exit
$ rake
...
(main)> Task.count
=> 2
(main)> t1, t2 = Task.all.array
=> [<Task: 0x8e04b30> (entity: Task; id: 0x8e872b0 <x-coredata://B3458A4C-4D78-4774-A620-E1DF16704E97/Task/p1> ; data: <fault>), <Task: 0x8e048f0> (entity: Task; id: 0x8e11390 <x-coredata://B3458A4C-4D78-4774-A620-E1DF16704E97/Task/p2> ; data: <fault>)]
(main)> t1.task_description
=> "Complete chapter"
(main)> t2.task_description
=> "File tax return"
(main)> t2.destroy
=> #<NSManagedObjectContext:0x914cbe0>
(main)> cdq.save
=> true
(main)> Task.count
=> 1

Looking good! With very little effort we have gotten a Core Data stack set up that is driven by an ActiveRecord-like schema that even supports versioning (which we will try next), all thanks to the amazing people at infinitered who have developed and released this library.

So now we can create, read, update and delete (destroy) records, with a great many options available to us when it comes to querying our data:

Task.where(:task_description).eq("Go running")
Task.where(:task_description).not_equal("Complete tax return")
Task.limit(1)
Task.offset(10)
Task.where(:task_description).contains("A").offset(10).first

# Conjuctions
Task.where(:task_description).contains("Complete").and.contains("tax")
Task.where(:task_description).begins_with("C").or(:task_description).begins_with("D")

Migrations

Whenever I am evaluating one of these Core Data wrappers (and there are several) my first question is whether migrations are supported or not. With CDQ this is largely a moot point as I will show in another post that performing lightweight migrations manually when you have an xcdatamodeld bundle is fairly straightforward. Still, it would be best if we could simply continue to call cdq.setup and have it handle that for us.

So far in our todo app all we can do is store tasks with no bearing even on whether they’ve been completed or not. So for our second iteration lets add a ‘completed’ boolean field to the Task model and see how CDQ handles this schema change.

To do this we start by taking our original schema and copy it to a new file:

$ cp schemas/001_initial.rb schemas/002_add_completed_field.rb

Now we edit this new file schemas/002_add_completed_field.rb, bumping the schema version and adding the new field definition:

schema "002" do
  entity "Task" do
    string :task_description, optional: false
    boolean :completed, default: false
  end
end

We then rebuild our schema so that our xcdatamodeld bundle will contain both schema versions:

$ rake schema:build
Generating Data Model learn-cdq
   Loading schemas/001_initial.rb
   Loading schemas/002_add_completed_field.rb
   Writing resources/learn-cdq.xcdatamodeld/001.xcdatamodel/contents
   Writing resources/learn-cdq.xcdatamodeld/002.xcdatamodel/contents

At this point we can simply build and run the app, and CDQ will handle the migration (or rather, CDQ will instruct Core Data to perform an automatic schema migration). We should then be able to confirm that our Task model now has a new ‘completed’ field:

(main)> task = Task.first
=> <Task: 0x8ccf830> (entity: Task; id: 0x8cced10 <x-coredata://B3458A4C-4D78-4774-A620-E1DF16704E97/Task/p1> ; data: <fault>)
(main)> task.task_description
=> "Complete chapter"
(main)> task.completed
=> nil

Ah; we specified in the schema that the completed field should default to being false but this isn’t going to affect existing records, which will have nill (NULL at the database level) in that field. New tasks should have the field set correctly however:

(main)> task2 = Task.create(task_description: "Write new blog post")
=> <Task: 0xa119980> (entity: Task; id: 0xa1199f0 <x-coredata:///Task/t3036D001-C532-4FDA-ABF2-CB7243B6E5C22> ; data: {
    completed = 0;
    "task_description" = "Write new blog post";
})
(main)> cdq.save
=> true
(main)> task2.completed
=> 0
(main)>

0 being logically false, this is working as expected.

A completed field should only either be false or true—a value of nil is logically false so our application logic will still work as expected but it would be cleaner if we could ensure that our new field can only have the two values we expect.

Let’s investigate what would have happened if we had additionally specified that the field is non-optional, which would ensure that the field is never nil/NULL.

We start by deleting the app from the simulator (or running rake simulator clean=1) to delete everything including the persistent store (which had been migrated to schema version 002) and the schema files. Be sure at this point to rebuild the schema, or else this change will not have any effect:

$ rake schema:build
Generating Data Model learn-cdq
   Loading schemas/001_initial.rb
   Writing resources/learn-cdq.xcdatamodeld/001.xcdatamodel/contents

Then we temporarily move schema version 002 out of the schemas directory so that we can initialise Core Data with the original schema version and create a task:

$ mv schemas/002_add_completed_field.rb ./
$ rake
...
(main)> task = Task.create(task_description: "Go for a run")
=> <Task: 0x8dedd50> (entity: Task; id: 0x8dedd80 <x-coredata:///Task/t089EFDD2-196B-45C5-AD3D-49CCDC5668832> ; data: {
"task_description" = "Go for a run";
})
(main)> task.task_description
=> "Go for a run"
(main)> task.completed
2014-01-25 15:40:17.073 learn-cdq[35446:80b] undefined method `completed' for #<Task_Task_:0x8dedd50> (NoMethodError)
=> #<NoMethodError: undefined method `completed' for #<Task_Task_:0x8dedd50>>
(main)> cdq.save
=> true
(main)> exit

So now we have a task saved that has a description as before but is back to having no ‘completed field’.

Now move the 002 schema back into the schemas directory:

$ mv 002_add_completed_field.rb schemas/

And update the ‘completed’ field definition to make it non-optional:

schema "002" do
  entity "Task" do
    string :task_description, optional: false
    boolean :completed, default: false, optional: false
  end
end

Rebuild the schema:

$ rake schema:build
Generating Data Model learn-cdq
   Loading schemas/001_initial.rb
   Loading schemas/002_add_completed_field.rb
   Writing resources/learn-cdq.xcdatamodeld/001.xcdatamodel/contents
   Writing resources/learn-cdq.xcdatamodeld/002.xcdatamodel/content

Automatic Schema Building

It can be tedious (not to mention error-prone) to always need to remember to build the schema at the right time. Forget to do it and you might be left puzzling over why fields are still defined even after they’ve been removed from the latest schema version. To avoid this, you can set up your build process to always build the schema when running in the simulator by adding the following to your Rakefile: task :"build:simulator" => :"schema:build" The reason why we haven’t done this from the start is to make it explicit when we do need to build the schema—there will come times when you will want to have finer control of this and might want to avoid rebuilding the schema when you rebuild the app; since you started by manually running the build command you will understand the process better than if we had set this earlier on. Personally, when working out the finer details of migrations I prefer to only build when I want to rather than every time but it is up to you which approach you take.

Now let’s build and run the app to see what happens with this migration:

$ rake
...
(main)> task = Task.first
=> <Task: 0x8e9af60> (entity: Task; id: 0x8e9a440 <x-coredata://A8445735-35DF-4032-8E2D-B1F52E89E76A/Task/p1> ; data: <fault>)
(main)> task.task_description
=> "Go for a run"
(main)> task.completed
=> 0

Great—now our completed field is always set to either true or false (1 or 0) and won’t at any point be undefined.

Where to next?

Book-Cover

This post is part of an upcoming book that I am writing on Core Data in RubyMotion. If you would like to learn more about using Core Data effectively in your RubyMotion apps please have a look.

Also coming up will be more blog posts where we will develop the tasks app further, delving into some of the more thorny issues that you might encounter when using Core Data in production. If more such posts would be of interest to you then you might want to follow me on Twitter.