🎬

Realmこと始め(React Native)

2024/03/08に公開

React NativeでローカルDBとしてRealmを使ってみたのでまとめてみます

Realm とは

React Nativeのアプリ内でデータを永続化して保持しておきたい場合、以下の3つが候補に上がってくると思います。

  • FirebaseやRDBMSを立てるなどして、アプリ外にデータを持つ
  • AsyncStorageを使用する
  • RealmやSQLiteなどのローカルDBを使用する

RealmはローカルDBとして使用されるケースが多いですが、Atlas App Servicesと連携すれば、複数端末間でデータを同期することも可能です。

これは個人的な意見ですが、普段からRDBMS(MySQLなど)を使用された経験がある場合は、だいぶ取っ掛かりやすいと思います。

https://realm.io/

AsyncStorageとの違い

そもそも私自身がAsyncStorageを使用したことがなく、あまり詳しくないので詳細は書けませんが、AsycStorageもローカルにデータを残すことが可能です。

Realmと異なる点はクエリでデータを取得したりすることができないこと。
データをkey:valueのJSON形式で残していく形になるので、複雑なデータ構造やデータ量が多くなる場合には不向きです。

またDate()などのObject形式でのデータ保持がAsyncStorageではできません。

https://reactnative.dev/docs/asyncstorage

Realm インストール

※ インストール済みの方は飛ばしていただいて大丈夫です。
以下は Ver.11.2.0 で動作確認済みです

Realmをインストール

$ npm install realm

Pod インストール

$ cd ios
$ pod install

Schema Objectを定義

DBのschemaを定義します。

例えばユーザーというデータを持ちたい場合

Realm.ts
import Realm from 'realm';

const userSchema = {
  name: 'User', // テーブル名
  properties: { // カラム
    _id: {
      type: 'objectId',
      default: () => new Realm.BSON.ObjectId(),
      indexed: true
    },
    name: 'string',
    email: 'string',
    plan: {type: 'int', default: 0}, // 0:Free, 1:Pro
    receipt: {type: 'string', optional: true},
    createdAt: {type: 'date', default: new Date()},
  },
  primaryKey: '_id', // 主キー
};

idcreatedAtなんかのデフォルト値をセットする場合はdefaultプロパティをセットしておくと楽です
また、カラムに対してindexedを追加するとインデックスを張ることが可能です

    _id: {
      type: 'objectId',
      default: () => new Realm.BSON.ObjectId(),
      indexed: true
    },

オプショナル(null許容)にしたい場合はoptionalをセットしておきます

    receipt: {type: 'string', optional: true},

その他、使用できるタイプ
https://www.mongodb.com/docs/realm/sdk/react-native/model-data/data-types/property-types/#std-label-react-native-supported-property-types

Realmの呼び出し

執筆時に確認のためドキュメントを読んでいたら、Providerがあって、それを使う方法が正式みたいです。
https://www.mongodb.com/docs/realm/sdk/react-native/quick-start/#configure-a-realm
本当は公式の方法を推奨しますが、以下の方法で私は使用してます。

Realm.ts
import Realm from 'realm';

const UserSchema = {(省略)}

// Realmの初期化
export const openRealm = () => {
  const config = {
    path: 'hogeApp',
    schema: [UserSchema,], // スキーマをセット
    schemaVersion: 1,
    // NOTE: 本番は絶対falseにすること
    deleteRealmIfMigrationNeeded: false,
  };

  return new Realm(config);
};

deleteRealmIfMigrationNeededは、本番リリースするときには必ずfalseにするか、configから消しておくことを忘れないようにしてください(デフォルトはfalse)
でないと、スキーマのバージョンを上げたり、カラムを追加などの変更があった場合にデータが自動で削除されてしまいます。
開発時は頻繁にカラム追加や変更したりすると思うので、trueにしておいた方がいちいちschemaVersionを上げなくて済みます。

configはこちらを参考に設定
https://www.mongodb.com/docs/realm-sdks/react/latest/types/Realm.BaseConfiguration.html

アプリをアップデートしていく中でマイグレーションをする必要が出て来る場合があると思いますが、その場合は以下の対応が必要です。

  • schemaVersionに+1をする
  • onMigrationにマイグレーション処理を追加

例えば、「Settingテーブルを追加して、Userと紐づけておきたい」となった場合、
SettingSchemaを追加して以下のようにマイグレーション処理を追記することになります。

config
import Realm from 'realm';

const UserSchema = {(省略)}
+ const SettingSchema = {
+   name: 'Setting',
+   properties: {
+     _id: {
+       type: 'objectId',
+       default: () => new Realm.BSON.ObjectId(),
+       indexed: true
+     },
+     userId: {type: 'string', indexed: true},
+     theme: {type: 'int', default: 0} // テーマ(0:ライト, 1:ダーク)
+   },
+   primaryKey: '_id',
+ };

// Realmの初期化
export const openRealm = () => {
  const config = {
    path: 'hogeApp',
-   schema: [UserSchema,],
+   schema: [UserSchema, SettingSchema], // スキーマを追加
-   schemaVersion: 1,
+   schemaVersion: 2, // Versionに+1
    // NOTE: 本番は絶対falseにすること
    deleteRealmIfMigrationNeeded: false,
+   // マイグレーション処理を追加
+   onMigration: (oldRealm, newRealm) => {
+     if (oldRealm.schemaVersion < 2) {
        // 紐づけたいユーザーを取得
+       const user = newRealm.objects('User')[0];
+       // Settingが作成されていないのでマイグレーションで作成する
+       newRealm.create('Setting', {
+         _id: new Realm.BSON.ObjectId(),
+         userId: String(user._id),
+         theme: 0
+       });
+     }
+   }
  };

  return new Realm(config);
};

CRUD処理

Create(データ作成)

import { openRealm } from '../realm';

// Realmの初期化
const realm = openRealm();

type UserProps = {
  name: string;
  email: string;
};

/**
 * Userを作成する
 * @param {Object} userData
 * @returns {Boolean}
 */
export const createUser = (data: UserProps): Boolean => {
  try {
    // DBにデータを保存
    realm.write(() => {
      realm.create('User', data);
    });
    return true;
  } catch (error) {
    console.error(error);
    return false;
  }
};

Read(データ取得)

/**
 * ユーザーを取得する
 * @param {string} id
 * @returns {UserProps}
 */
export const getLoginUser = (id: string): UserProps => {
  let user = <UserProps>{};
  try {
    let users = realm.objects('User') ?? [];
    user = users.filtered('isLogin == $0', true)[0];
  } catch (error) {
    console.error(error);
  }
  return user;
};

絞り込み検索をする場合filteredで絞り込みがかけれる
検索かけたい値を引数にセットする。引数は$0で表す。複数ある場合は$0,$1,$2となる

filtered
  const today = new Date();
  const start = new Date(target);

  let users = realm.objects('User') ?? [];
  const user = users.filtered('date >= $0 AND date <= $1', start, today);

またsortedで降順、昇順の切り替えが可能

filterd + sorted
  const today = new Date();
  const start = new Date(target);

  let users = realm.objects('User') ?? [];
  const user = users
    .filtered('date >= $0 AND date <= $1', start, today)
+   .sorted('date', false); // 降順で並び替え

Update(更新)

/**
 * ユーザー情報をアップデートする
 * @param {UserProps}
 * @returns {Boolean}
 */
export const updateUser = (data: UserProps): Boolean => {
  try {
    let user: UserProps = realm.objectForPrimaryKey('User', data._id);
    realm.write(() => {
      user.name = data.name;
      user.email = data.email;
    });
    return true;
  } catch (error) {
    console.error(error);
    return false;
  }
};

primaryKeyでオブジェクト取得する場合はobjectForPrimaryKeyを使用する

objectForPrimaryKey
  let user = realm.objectForPrimaryKey('User', User._id);

Delete(削除)

/**
 * ユーザーを削除する
 * @param {string} id
 * @returns {boolean}
 */
export const deleteUserById = (id: string): boolean => {
  try {
    let user = realm.objectForPrimaryKey('User', id);
    realm.write(() => {
      realm.delete(user);
    });
    return true;
  } catch (error) {
    console.error(error);
    return false;
  }
};

注意しないといけないのが、データ削除後にView側で削除したrealmデータを参照しに行ってしまいエラーとなる現象があるので、削除後は空のオブジェクトをセットする必要があります。

参考

https://www.mongodb.com/docs/realm/sdk/react-native/

https://realm.io/

GitHubで編集を提案

Discussion