Database Manager (Swift)— Micro Realm Universe (Part I)

Khwan Siricharoenporn
te<h @TDG

--

Realm Microservice.

Keywords:

  • Database Manager (AKA. DM)
  • Realm Database (AKA. RD)
  • General Storage (is a long-term memory)
  • Testing Storage (is a short-term memory)

Let’s not waste time. This will clarify how you can use Database Manager and what should be in it. The common element of the DM has three components. Each component has a different responsibility. DM will combine each element and make a powerful class to handle database systems.

Which in the real world programming by using the RD. That is the best way to work with the top of the database platform, but RD has any duplicate code. This dependency may make some annoying for you. The DM is a hero for this problem, because it will contain important logics to solve your needs.

Try to imagine that you need to change the database version, implement a new migration and switch memory. It is a nightmare, right? Using Database Manager will help you manage all of these.

Let’s start.

Sections:

  1. What is the Database Manager?
  2. Storage type
  3. Version Management
  4. Migration Management
  5. Implementation
  6. How to use

Section 1: What is the Database Manager?

The DM is a tool that can be use for the following:

  • Managing the database
  • Increasing convenience
  • Ease of access
  • Connection to the data layer

Section 2: Storage type

For iOS platform when you need to store some data. It doesn’t matter to keep in general the default document or path. You can store Realm files everywhere in the project bundle, but you must implement many lines of code to specify some path of the bundle to store Realm files. The Type of Storage type has two and each type has different work.

General storage

The DM have a capacity for long term memory like HDD and SSD. Normally when Realm has been initialized each time. It will create Realm files by default path and let’s store to bundle, but if you don’t want to store Realm files at default path. General storage therefore necessarily can customize a path of Realm files by developer.

Testing storage

It also has a short time memory for example RAM. This storage can be used specifically for the following.

  • Unit-test
  • Short live data

Section 3: Version Management

The DM should have a version management. The database when created and run. The database will generate table and self version for identification of some abilities and some configuration.

Trust me. This will make your life easy because Version management will assist you to memorize and manage some configuration for easily handling work.

Section 4: Migration Management

The last most important component is Migration Management. Each version of the database may have different fields or data types of each field. Responsible for Migration Management is specified that the previous version has some differences with the current version. It will compare and fix for conflict data automatically by guideline developer implementation. If you don’t have this component. The database will crash when you release a new version.

Section 5: Implementation

Fun part. Try to implement what you have read above. Which resources are needed?

Resources:

  1. New Xcode project.
  2. Already installed RealmSwift dependency.
  3. Already installed Realm Studio.

If you already have it. Let’s start!

Step 1:

Create a file and define a protocol DatabaseConfigurable.

protocol DatabaseConfigurable {
var schemaName: String? { get }
var schemaVersion: UInt64? { get }
var objectTypes: [Object.Type]? { get }
var migrationBlock: MigrationBlock? { get }
}

When you implement DatabaseConfigurable. You must conform according to standard of protocol. How to use each property I will clarify it.

schemaName

This property you must assign the database name that you want. For the Microservice concept you should define one schemaName per one table. If you want to use legacy architecture you can assign nil for it.

schemaVersion

This property you must assign the database version according to the migration count. For the Microservice concept you should define one schemaVersion per one table. If you want to use legacy architecture you can assign nil for it. If you assign nil. You must change the version on kLegacySchemaVersion instead according to the migration count too.

objectTypes

This property you must assign the table type to verify that this schema will use only this table. For the Microservice concept you should define one objectTypes per one table. If you want to use legacy architecture you can assign nil for it.

migrationBlock

This property you must implement the migration logic if a table has changed. For the Microservice concept you should define one migrationBlock per one table. If you want to use legacy architecture you can assign nil for it. If you assign nil. You must implement the migration logic on legacyMigration instead.

Step 2:

Create an extension in the new file and define name is DatabaseConfigurable+Setup.

fileprivate let kLegacySchemaVersion: UInt64 = 0
fileprivate let kDocumentName: String = “Database”
//MARK: Setup
extension DatabaseConfigurable {
var `extension`: String {
return “realm”
}
var fileName: String {
var fileName: String

if let schemaName = schemaName, !schemaName.isEmpty {
fileName = schemaName + “.” + `extension`
}
else {
fileName = “Default”
}
return fileName
}
var absolutelySchemaVersion: UInt64 {
var version: UInt64
if let schemaVersion = schemaVersion {
version = schemaVersion
}
else {
version = kLegacySchemaVersion
}
return version
}
var absolutelyMigrationBlock: MigrationBlock {
var migration: MigrationBlock
if let migrationBlock = migrationBlock {
migration = migrationBlock
}
else {
migration = legacyMigration
}
return migration
}
var path: URL? {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
guard let document = paths.first, let documentUrl = URL(string: document) else { return nil } let path = documentUrl.appendingPathComponent(kDocumentName) if !FileManager.default.fileExists(atPath: path.absoluteString) {
do {
try FileManager.default.createDirectory(atPath: path.absoluteString, withIntermediateDirectories: true, attributes: nil)
}
catch {
print(“Cannot create database directory!: \(error.localizedDescription)”)
return nil
}
}
let absolutelyPath = path.appendingPathComponent(fileName) return absolutelyPath
}
}

kLegacySchemaVersion

Please read schemaVersion description (in Step 1).

kDocumentName

This is just a document name for storing Realm files.

extension

Use for specify extension of Realm files.

fileName

Use for specify Realm file name. It will get data from schemaName (in Step 1) and merge with extension.

absolutelySchemaVersion

This property will decide automatically to use between schemaVersion (in Step 1) and kLegacySchemaVersion.

absolutelyMigrationBlock

This property will decide automatically to use between migrationBlock (in Step 1) and legacyMigration.

path

Use for specify path for storing Realm files.

Step 3:

Create an extension in the new file and define name is DatabaseConfigurable+LegacyMigration.

//MARK: Legacy Migration
extension DatabaseConfigurable {
var legacyMigration: MigrationBlock {
return { (migration, oldSchemaVersion) in

}
}
}

Please read migrationBlock description (in Step 1).

Step 4:

Create a new enum type is RealmMemoryType.

enum RealmMemoryType {
case inStorage
case inMemory(identifier: String?)
var associated: String? {
switch self {
case .inStorage:
return nil
case .inMemory(let identifier):
return identifier
}
}
}

This enum is used to specify a type of memory to store some data or do migration.

inStorage

is a General Storage.

inMemory

is a Testing Storage or Short time storage.

Step 5:

Create a new enum type is RealmErrorType.

enum RealmErrorType: Error {
case realmIsEmpty
case configurationFailure
}

This enum is a new error type only used for the DM. When you need to initialize a new Realm instance. If you have any problems while initializing them. You should know that error is what kind of type.

realmIsEmpty

When you can’t initialize the Realm instance. You will get this error.

configurationFailure

When you do something about configuration. If Realm can’t be configured. You will get this error.

Step 6:

Create an extension in the new file and define name is DatabaseConfigurable+Configuration.

//MARK: Configure
extension DatabaseConfigurable {
private func configuration(memoryType: RealmMemoryType) throws -> Realm.Configuration {
switch memoryType {
case .inStorage: return try inStorageConfigure()
case .inMemory: return try inMemoryConfigure(identifier: memoryType.associated)
}
}
private func inStorageConfigure() throws -> Realm.Configuration {
let realmConfig = Realm.Configuration(fileURL: path,
readOnly: false,
schemaVersion: absolutelySchemaVersion,
migrationBlock: absolutelyMigrationBlock,
objectTypes: objectTypes)
guard realmConfig.fileURL != nil else { throw RealmErrorType.configurationFailure } return realmConfig
}
private func inMemoryConfigure(identifier: String?) throws -> Realm.Configuration {
if let identifier = identifier {
var realmConfig = Realm.Configuration(inMemoryIdentifier: identifier)
realmConfig.readOnly = false

return realmConfig
}
else {
throw RealmErrorType.configurationFailure
}
}
func realm(with memoryType: RealmMemoryType) -> Realm? {
guard let configuration = try? self.configuration(memoryType: memoryType) else { return nil }
guard let realm = try? Realm(configuration: configuration) else { return nil } return realm
}
}

For this part. It’s a configuration part of DatabaseConfigurable protocol. When you want to get Realm instances for store, fetch, delete, update something in the database. You must call realm(with memoryType: ) method. This method will do configuration(memoryType: ) throws -> Realm.Configuration to choose type of memory for the Realm.Configuration instance.

inStorageConfigure() throws -> Realm.Configuration

Use when you want to get storage configuration. You just choose enum inStorage. This method will get all important information property from the self instance.

inMemoryConfigure(identifier: String?) throws -> Realm.Configuration

Use when you want to get memory configuration. You just choose an enum inMemory type and assign an identifier.

This is all for the DM. In the next section I will show you how to use it on the real project.

Section 6: How to use

Step 1:

Create a new file and define class name as StudentRepository then implement Object, DatabaseConfigurable protocols.

class StudentRepository: Object {
@objc dynamic var id: String = “”
@objc dynamic var name: String = “”
override static func primaryKey() -> String {
return “id”
}
}
//MARK: DatabaseConfigurable
extension StudentRepository: DatabaseConfigurable {
var schemaName: String? {
return “Student” //Assign your database name
}
var schemaVersion: UInt64? {
return 0 //It always starts at 0. If you use the Microservice concept.
}
var objectTypes: [Object.Type]? {
return [StudentRepository.self] //Verify your database that can allow only this table.
}
var migrationBlock: MigrationBlock? {
return nil /*If you have any change about this table and use the Microservice concept. You must implement the migration logic here. (You can implement Realm behavior.)*/
}
}

Step 2:

Create a new file and define class name as ClassRoomRepository then implement Object, DatabaseConfigurable protocols.

class ClassRoomRepository: Object {
@objc dynamic var id: String = “”
@objc dynamic var name: String = “”
let studentIds: List<String> = List<String>()
override static func primaryKey() -> String {
return “id”
}
}
//MARK: DatabaseConfigurable
extension ClassRoomRepository: DatabaseConfigurable {
var schemaName: String? {
return “ClassRoom” //Assign your database name
}
var schemaVersion: UInt64? {
return 0 //It always starts at 0. If you use the Microservice concept.
}
var objectTypes: [Object.Type]? {
return [ClassRoomRepository.self] //Verify your database that can allow only this table.
}
var migrationBlock: MigrationBlock? {
return nil /*If you have any change about this table and use the Microservice concept. You must implement the migration logic here. (You can implement Realm behavior.)*/
}
}

Step 3:

Implement addStudent() and addClassRoom() methods on ViewController.

class ViewController: UIViewController {
var student: StudentRepository!
override func viewDidLoad() {
super.viewDidLoad()
addStudent()
addClassRoom()
}
func addStudent() {
student = StudentRepository()
student.id = 1.description
student.name = “Josh”
let realm = student.realm(with: .inStorage) //You must initialize Realm instance from any Repository, because the Repository will verify a table and database. try! realm?.write {
realm?.add(student, update: .all)
}
}
func addClassRoom() {
let classRoom = ClassRoomRepository()
classRoom.id = 1.description
classRoom.name = “A room”
classRoom.studentIds.append(student.id)
let realm = classRoom.realm(with: .inStorage) //You must initialize Realm instance from any Repository, because the Repository will verify a table and database. try! realm?.write {
realm?.add(classRoom, update: .all)
}
}
}

Step 4:

You will get the result like the image below.

Realm files

The database was generated according to a class of the Object protocol. For example, I created two tables. Realm has generated two schemas. Each schema has only one table.

Step 5:

You can access Realm files to see the result.

Student database
ClassRoom database

You can manage all tables easier and don’t worry about schema dependencies. This is just the example. You can adapt it to suit your work or requirements. In the next article. I will clarify about the Multithreading Application & Realm thread problem. Follow me to read the next article. You can share any idea in the comment.

Please clap and share.

Thank you!!

--

--

Khwan Siricharoenporn
te<h @TDG

iOS Developer @Central Group. Interested about core code, low-level, design pattern and architecture.