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:
In this example, we get
SELECT ID, NAME FROM USERS WHERE ID = 3
nameof 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:
ORM will automatically translate it into database syntax for us.
user = User.where("id = 3")
There are lots of ways to work with databases in iOS:
Core Data, which is a default ORM for an
SQLitedatabase. It is a very good choice. However, it is very difficult to learn;
SQLitedatabase. This may be easier for people who are familiar with
SQLsyntax, but for a beginner it can be not the best option;
is stable, documented, and developed by Apple, we are not going to
use it in this tutorial.
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.
There are different kinds of databases: relational DB, key-value data
stores, etc. Realm is a relational DB - it uses
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
Creating a schema is very easy, but has some Objective-C specific. For
example to create a class in Ruby, we just use
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
class SomeClass # code end
.hAll method definitions are located in another (implementation) file. Extension of implementation file is
However, it is also possible to keep both Interface and Implementation in
.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:
If we could write schemas in Ruby, it would look like:
// 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
The biggest difference here is that Ruby is a dynamic language, and we don't set variable's type.
class SomeClass attr_accessor :created_at, :index end class OtherClass attr_accessot :created_at, :index end
To create a valid schema class,
we need to include Realm first, and then base all classes on a
This is it! Realm will create a MainClass table and OtherClass table.
// 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
MainClasstable will have two properties:
OtherClasswill have two properties as well:
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.
More on relationships can be found at Realm's docs: Relationships
#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
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
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
Later you decided to merge them into
#import <Realm/Realm.h> @interface Person : RLMObject @property NSString *first_name; @property NSString *last_name; @end
nameproperty. Your DB already has some Person records, and it will need to know what to do with deleted columns, and how to populate new
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
gem your migration code will look like this:
This is it! During migration app will just merge
# 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
last_namevalues into a new
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
property, migration could look like this:
Remember to have schema version checks for all your available schema versions. Also don't use nested
# 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
if/elif/endstatements - 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
classes, we would create two new files:
With next content:
# child.rb: class Child include MotionRealm end # parent.rb class Parent include MotionRealm end
To create instance of a
class, simply use next command:
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 = Child.new parent = Parent.new
There is no difference between first and second way. In fact,
child.save # or in a block: RLMRealm.write do |realm| # add object to the database: realm << parent end
savemethod just runs the same block, the developer just does not see it (you can find it in
motion-realmsources if curious).
To retrieve data from the database you can use
method on a class:
Alternative way is to use
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")
Apple has lots of info on predicates. Or you can check Realm's NSPredicate Cheatsheet
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
If you want to delete an object, it can be done in two ways:
Just like with saving,
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
deletemethod is just a shorthand for an
Sometimes you will want to be notified when data inside your Realm
changes. Each time write transaction is committed, Realm sends a
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
MotionRealmmodule. You can learn more about Realm at their official page. To find out more about
motion-realm, visit its Github page.