First, let's talk about databases and ORMs.
The database is just a collection of information organized in some way. To interact with data, each database has its own set of commands. For SQL databases, it is SQL, that looks like:
SELECT ID, NAME FROM USERS
WHERE ID = 3
In this example, we get
ID
and
name
of a user with
ID = 3
Since not every developer familiar with database syntax, ORMs were introduced. ORM stands for Object-Oriental Mapping and helps us to manipulate the data with the programming language of choice instead of database command. If we wanted to find a user with ID = 3 with ORM, we would use syntax that could look like this:
user = User.where("id = 3")
ORM will automatically translate it into database syntax for us.
There are lots of ways to work with databases in iOS:
Core Data
, which is a default ORM for an
SQLite
database. It is a very good choice.
However, it is very difficult to learn;
SQLite
database. This may be easier for people who are familiar with
SQL
syntax, but for a beginner it can be not the best
option;
Even though
Core Data
is stable, documented, and developed by Apple, we are not going to
use it in this tutorial.
Core Data
is a very complex framework - it is hard to understand for a beginner,
it needs lots of additional and difficult code to make it work,
documentation is outdated and is not always correct.
Here at Mova, we always try to use Realm when possible. It is stable, the code is very easy to read and understand, and it is also very fast.
There are different kinds of databases: relational DB, key-value data
stores, etc. Realm is a relational DB - it uses
tables
to store data. The table has columns
and rows (records). Here is the simple example of how does table in a
relational DB looks like. In this example we use "Company" table:
companies = Company.where("name = 'Company 1'")
# => array of companies where name = 'Company 1'
company = company.first
company.name
# => "Company 1"
company.code
# => "c1"
Database Schemas are structures that describe tables with its properties and property types in a Database. The database won't store any data if it has no tables, and table won't store any data if it does not know which properties of which type can be stored.
In a given example with Companies, schema has only 1 table with next properties:
Realm schemas are written in Objective-C.
Their code is very easy to understand
if you are familiar with Objective-C. Otherwise,
you can check comments next to each line. In Objective-C comments
start with
//
.
Creating a schema is very easy, but has some Objective-C specific. For
example to create a class in Ruby, we just use
class SomeClass
# code
end
However in Objective-C first we create some kind of description (header)
file, which has a list of all properties and methods of the class.
This description is called Interface. Its
file extension should always be
.h
All method definitions are located in another (implementation) file.
Extension of implementation file is
.m
.
However, it is also possible to keep both Interface and Implementation in
implementation (.m
) file. But you will still
need to have Interface at the beginning
of the file.
Let's see how an empty class will look like. Let's use one file for simplicity:
// describe MainClass' properties:
@interface MainClass
@property NSDate *created_at;
@property NSString *name;
@end
// describe OtherClass' properties:
@interface OtherClass
@property NSDate *created_at;
@property NSInteger *index;
@end
// Implementation part:
// create MainClass class
@implementation MainClass
@end
// create OtherClass class
@implementation OtherClass
@end
If we could write schemas in Ruby, it would look like:
class SomeClass
attr_accessor :created_at, :index
end
class OtherClass
attr_accessot :created_at, :index
end
The biggest difference here is that Ruby is a dynamic language, and
we don't set variable's type.
To create a valid schema class,
we need to include Realm first, and then base all classes on a
RLMObject
:
// include Realm:
#import <Realm/Realm.h>
// MainClass is a subclass of RLMObject:
@interface MainClass : RLMObject
@property NSDate *created_at;
@property NSString *name;
@end
// OtherClass is a subclass of RLMObject:
@interface OtherClass : RLMObject
@property NSDate *created_at;
@property NSInteger *index;
@end
// And implementation:
// create MainClass class
@implementation MainClass
@end
// create OtherClass class
@implementation OtherClass
@end
This is it! Realm will create a MainClass table and OtherClass table.
MainClass
table will have two properties:
created_at
with
Date
type, and
name
with a
String
type.
OtherClass
will have two properties as well:
created_at
with
Date
type, and
index
with an
Integer
type.
Realm can use next classes for its properties:
Sometimes you need to connect one object to another. In these cases,
we will use
relationships
. For example, we have Child and Parent
classes. A Child can have only one father and only one mother. However,
parents can have multiple children. It means that Child will have
two properties of Parent class: father and mother. On the other hand,
Parent will have one property children, of an Array type.
#import <Realm/Realm.h>
// Class Child will use Class Parent BEFORE it has been declared.
// We need to make Objective-C compiler understand that this is not a
// mistake by using next command:
@class Parent;
@interface Child : RLMObject
@property BOOL likes_math;
@property NSInteger favourite_number;
@property NSString *name;
@property NSString *sex;
@property NSDate *birthday;
// Child has one mother, and one father:
@property Parent *mother;
@property Parent *father;
@end
// Declare a custom type, array of Child objects:
RLM_ARRAY_TYPE(Child)
@interface Parent : RLMObject
@property NSString *job;
@property NSString *name;
@property NSString *sex;
@property NSDate *birthday;
@property BOOL likes_ruby;
@property NSInteger apps_in_appstore;
// Parent can have many children. We create an
// array of Child objects with syntax like this:
@property RLMArray *children;
@end
More on relationships can be found at Realm's docs:
Relationships
Relationships can be used in code like this:
son = Child.new
son.name = "John"
daughter = Child.new
daughter.name = "Lily"
father = Parent.new
father.name = "Jack"
# create `father` relation:
son.father = father
daughter.father = father
# create `children` relation:
father.children << son
father.children << daughter
# in a future you can always get parent of children relation:
parent = Parent.where("name = 'Jack'").first
parent.children
# => returns array of parent's children, in our case John and Lily
lily = Child.where("name = 'Lily'").first
lily.name
# => "Lily"
jack = lily.parent
# => Parent object linked to Lily
jack.name
# => "Jack"
Schema changes are inevitable. Property names can be changed or new ones added, new classes created or removed, etc. If you just add your
changes to the
schema.m
file, it will not work, at least because the app won't know what to do with
new or deleted properties. For example, you had a schema Person, that has
last_name
and
first_name
:
#import <Realm/Realm.h>
@interface Person : RLMObject
@property NSString *first_name;
@property NSString *last_name;
@end
Later you decided to merge them into
name
property. Your DB already has some Person records, and it will need
to know what to do with deleted columns, and how to populate new
name
column.
To change DB schema, we are going to use migrations. This is a small part
of the code that lets us tell the app how to update DB with older schema
to a newer DB schema. Using
motion-realm
gem your migration code will look like this:
# versions start from 0. Next version is 1.
RLMRealmConfiguration.migrate_to_version(1) do |migration, old_version|
# Tell the app what to do with updated Person objects:
migration.enumerate "Person" do |old_object, new_object|
# user has not updated his app to the 1st schema version yet:
if old_version < 1
new_object.name = old_object.first_name + " " + old_object.last_name
end
end
end
This is it! During migration app will just merge
first_name
and
last_name
values into a new
name
field.
It is important to have schema version checks for all
schema version you had: some users
update their apps more often, some less. Imagine user X updated the app
in time, and his schema has been updated, but user Y installed the app
and did not update it for three months. DB schema version on his device
is 0, and your latest update already has 2nd schema version.
In this case,
his DB needs to be updated step by step, from 0 to 1st, from 1st to 2nd.
If your 2nd schema version adds
age
property, migration could look like this:
# versions start from 0. Next version is 1.
RLMRealmConfiguration.migrate_to_version(1) do |migration, old_version|
# Tell the app what to do with updated Person objects:
migration.enumerate "Person" do |old_object, new_object|
# user has not updated his app to the 1st schema version yet:
if old_version < 1
new_object.name = old_object.first_name + " " + old_object.last_name
end
if old_version < 2
new_object.age = 18
end
end
end
Remember to have schema version checks for all your available schema
versions. Also don't use nested
if/elif/end
statements - they will skip migration to latest version:
# versions start from 0. Next version is 1.
RLMRealmConfiguration.migrate_to_version(1) do |migration, old_version|
# Tell the app what to do with updated Person objects:
migration.enumerate "Person" do |old_object, new_object|
# imagine old schema version is 0. It will be migrated to the
# first version only:
if old_version < 1
new_object.name = old_object.first_name + " " + old_object.last_name
elsif old_version < 2
new_object.age = 18
end
end
end
We know how to create schemas and how to update them. Let's figure out how can we create, read and delete objects.
First we will need to create a model for each class. In our case
with
Child
and
Parent
classes, we would create two new files:
/app/models/child.rb
and
/app/models/parent.rb
With next content:
# child.rb:
class Child
include MotionRealm
end
# parent.rb
class Parent
include MotionRealm
end
To create instance of a
Child
or
Parent
class, simply use next command:
child = Child.new
parent = Parent.new
Remember that objects are not persisted yet! They exist only in
memory,
and they will be gone next time the user opens the app. You can save them
in two
ways:
child.save
# or in a block:
RLMRealm.write do |realm|
# add object to the database:
realm << parent
end
There is no difference between first and second way. In fact,
save
method just runs the same block, the developer just does not see it (you
can find it in
motion-realm
sources if curious).
To retrieve data from the database you can use
where
method on a class:
children = Child.where("name = 'John'")
# => array of children with name 'John'
# if you want to get all objects:
all_children = Child.all
# or just first child:
first_child = Child.first
# or
first_child = Child.all.first
last_child = Child.last
# if you need to specify multiple parameters:
children = Child.where("name BEGINSWITH 'J' AND age > 10")
Alternative way is to use
NSPredicate
:
predicate = NSPredicate.predicateWithFormat "name == %@", "some cool name"
cool_kids = Child.with_predicate predicate
predicate = NSPredicate.predicateWithFormat "name CONTAINS %@ AND age < %@", "cool", 16
kids = Child.with_predicate predicate
Apple has
lots of info on predicates.
Or you can check Realm's
NSPredicate Cheatsheet
If you want to delete an object, it can be done in two ways:
child.delete
# or in a block:
RLMRealm.write do |realm|
# remove from database:
realm.delete parent
end
# if you want to delete all objects of a class:
Parent.delete_all
# or to clear the whole DB:
RLMRealm.write do |realm|
realm.delete_all
end
Just like with saving,
delete
method is just a shorthand for an
RLMRealm.write
block.
Sometimes you will want to be notified when data inside your Realm
changes. Each time write transaction is committed, Realm sends a
notification. With
motion-realm
gem you can subscribe to them like this:
@notification_token = realm.add_notification do |notification, realm|
# some your actions here:
p "Just updated Database!"
end
# don't forget to remove it when you no longer need it:
realm.remove_notification @notification_token
RLMObject
, and to add all properties that we are going to
use in the app.
Later we will also need to create a Ruby class with the same name, and include
MotionRealm
module.
You can learn more about Realm at their
official page.
To find out more about
motion-realm
,
visit its
Github page.