MotionRecord

Miniature ActiveRecord for RubyMotion

Everything you need to start using SQLite as the datastore for your RubyMotion app.

:turtle: Android support should be coming soon

Gem Version Code Climate Test Coverage

Installation

Add this line to your Gemfile:

gem "motion_record"

On iOS, MotionRecord uses motion-sqlite3 as a wrapper for connecting to SQLite, so add these too:

gem "motion-sqlite3"
# Requires the most recent unpublished version of motion.h
# https://github.com/kastiglione/motion.h/issues/11
gem "motion.h", :git => "https://github.com/kastiglione/motion.h"

And then execute:

$ bundle

MotionRecord::Base

MotionRecord::Base provides a superclass for defining objects which are stored in the database.

class Message < MotionRecord::Base
  # That's all!
end

Attribute methods are inferred from the associated SQLite table definition.

message = Message.new(subject: "Welcome!")
# => #<Message: @id=nil @subject="Welcome!" @body=nil, @created_at=nil ...>

Manage persistence with create, save, destroy, and persisted?

message = Message.create(subject: "Welcome!")
message.body = "If you have any questions, just ask us :)"
message.save
#    SQL: UPDATE messages SET subject = ?, body = ?, ... WHERE id = ?
# Params: ["Welcome!", "If you have any questions, just ask :)", ..., 1]
message.destroy
message.persisted?
# => false

Timestamp Columns

If any of the columns are named created_at or updated_at then they are automatically serialized as Time objects and set to Time.now when the record is created or updated.

MotionRecord::Schema

Define and run all pending SQLite migrations with the up! DSL.

def application(application, didFinishLaunchingWithOptions:launchOptions)
  MotionRecord::Schema.up! do
    migration 1, "Create messages table" do
      create_table :messages do |t|
        t.text    :subject,      null: false
        t.text    :body
        t.integer :read_at
        t.integer :remote_id
        t.float   :satisfaction, default: 0.0
        t.timestamps
      end
    end

    migration 2, "Index messages table" do
      add_index :messages, :remote_id, :unique => true
      add_index :messages, [:subject, :read_at]
    end
  end
  # ...
end

Schema Configuration

By default, MotionRecord will print all SQL statements and use a file named "app.sqlite3" in the application's Application Support folder. To disable logging (for release) or change the filename, pass configuration options to up!

resource_file = File.join(NSBundle.mainBundle.resourcePath, "data.sqlite3")
MotionRecord::Schema.up!(file: resource_file, debug: false) # ...

You can also specify that MotionRecord should use an in-memory SQLite database which will be cleared every time the app process is killed.

MotionRecord::Schema.up!(file: :memory) # ...

MotionRecord::Scope

Build scopes on MotionRecord::Base classes with where, order and limit.

Message.where(body: nil).order("read_at DESC").limit(3).find_all

Run queries on scopes with exists?, first, find, find_all, pluck, update_all, and delete_all.

Message.where(remote_id: 2).exists?
# => false
Message.find(21)
# => #<Message @id=21 @subject="What's updog?" ...>
Message.where(read_at: nil).pluck(:subject)
# => ["What's updog?", "What's updog?", "What's updog?"]
Message.where(read_at: nil).find_all
# => [#<Message @id=20 ...>, #<Message @id=21 ...>, #<Message @id=22 ...>]
Message.where(read_at: nil).update_all(read_at: Time.now.to_i)

Run calculations on scopes with count, sum, maximum, minimum, and average.

Message.where(subject: "Welcome!").count
# => 1
Message.where(subject: "How do you like the app?").maximum(:satisfaction)
# => 10.0

MotionRecord::Serialization

SQLite has a very limited set of datatypes (TEXT, INTEGER, and REAL), but you can easily store other objects as attributes in the database with serializers.

Built-in Serializers

MotionRecord provides a built-in serializer for Time objects to any column datatype.

class Message < MotionRecord::Base
  serialize :read_at, :time
end

Message.create(subject: "Hello!", read_at: Time.now)
#    SQL: INSERT INTO messages (subject, body, read_at, ...) VALUES (?, ?, ?...)
# Params: ["Hello!", nil, 1420099200, ...]
Message.first.read_at
# => 2015-01-01 00:00:00 -0800

Boolean attributes can be serialized to INTEGER columns where 0 and NULL are false and any other value is true.

class Message < MotionRecord::Base
  serialize :satisfaction_submitted, :boolean
end

Objects can also be stored to TEXT columns as JSON.

class Survey < MotionRecord::Base
  serialize :response, :json
end

survey = Survey.create(response: {nps: 10, what_can_we_improve: "Nothing :)"})
#    SQL: INSERT INTO surveys (response) VALUES (?)
# Params: ['{"nps":10, "what_can_we_improve":"Nothing :)"}']
survey
# => #<Survey: @id=1 @response={"nps"=>10, "what_can_we_improve"=>"Nothing :)"}>

RubyMotion doesn't have a Date class, but as long as you're okay with using Time objects with only the date attributes, you can serialize them to TEXT columns:

class User < MotionRecord::Base
  serialize :birthday, :date
end

drake = User.create(birthday: Time.new(1986, 10, 24))
#    SQL: INSERT INTO users (birthday) VALUES (?)
# Params: ["1986-10-24"]
# => #<User: @id=1, @birthday=1986-10-24 00:00:00 UTC>

Custom Serializers

To write a custom serializer, extend MotionRecord::Serialization::BaseSerializer and provide your class to serialize instead of a symbol.

class MoneySerializer < MotionRecord::Serialization::BaseSerializer
  def serialize(value)
    raise "Wrong column type!" unless @column.type == :integer
    value.cents
  end

  def deserialize(value)
    raise "Wrong column type!" unless @column.type == :integer
    Money.new(value)
  end
end

class Purchase < MotionRecord::Base
  serialize :amount_paid_cents, MoneySerializer
end

MotionRecord::Association

TODO

Contributing

Please do!

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request