あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

React NativeでRealmを使用する

はじめに

React Native で Realm を使用するには、結構ハマりどころが多く、苦戦しがちだと思います。

この記事では、React Native で Realm を使用するサンプルをご紹介します。

なお、Realm がサポートする node.js のバージョンは v8.x か v10.x のみなので注意が必要です。

node.jsの最新バージョンは v12.x になっていますが、v10.16.0 をこのサンプルでは使用しています。

Realm.js 6.0.0 から、Realm がサポートするバージョンが node.js の v10 以降になりました。

そのため、node.js の最新バージョンの v12.16.1 で動作確認をし直しました。(2020/07/26更新)

realm.io

node.js のバージョンの切り替えは、nodenv を使用しています。nodenv については以下の記事を参考にしてください。

www.aruse.net

サンプルコード

この記事で紹介するサンプルコードは、GitHub に上げてありますので必要に応じ参考にしてください。

github.com

使用するライブラリ

まず、Realm のライブラリを使用します。最新の v5.0.1 は不具合があるので、v3.6.5を使用します。

Realm 6.0.3 で動作確認をしたところ、問題は解消されていたので最新バージョンを使用します。(2020/07/26更新)

Realm には、id の AUTOINCREMENT 機能がないので、主キーに UUID を使用します。そのためのライブラリとして、uuidと、その補助ライブラリとしてreact-native-get-random-valuesを使用します。

なお、react-native-get-random-values は分かりにくいのですが、MIT ライセンスになります。

www.npmjs.com

github.com

ログに OS 情報を出力するために、react-native-device-infoというライブラリも使用します。

プロジェクトの作成

React Native のプロジェクトを作成します。今回は TypeScript を使用するので、以下のようにコマンドを実行します。

$ npx react-native init ReactNativeRealm --template react-native-template-typescript

ライブラリのインストール

ライブラリは以下の手順でインストールします。

普通にインストールすると不具合が起きるので、手順通りインストールしてください。

$ yarn add realm
$ yarn add uuid @types/uuid
$ yarn add react-native-get-random-values
$ yarn add react-native-device-info
$ cd ios
$ pod cache clean Realm
$ pod cache clean RealmSwift
$ pod deintegrate || rm -rf Pods
$ pod install --verbose
$ rm -rf ~/Library/Developer/Xcode/DerivedData
$ cd ..

なお、iOS の実行時にエラーが出てビルドが失敗するようになりました。対処方法は、下記記事を参照してください。(2020/07/26追記)

www.aruse.net

www.aruse.net

React Native で Realm を使用する

import 文(共通)

import 文は以下の並び順で指定してください。

import React, {useEffect} from 'react';
import {SafeAreaView, Text} from 'react-native';
import Realm from 'realm';
import DeviceInfo from 'react-native-device-info';
import 'react-native-get-random-values';
import {v4 as uuid} from 'uuid';

スキーマ定義(共通)

ここでは2つのスキーマを定義します。

BookSchema では主キーとインデックスの両方を定義しています。

// Schema definition
const PersonSchema = {
  name: 'Person',
  properties: {
    name: 'string', // required
    age: 'int?', // optional
  },
};

const BookSchema = {
  name: 'Book',
  primaryKey: 'id',
  properties: {
    id: 'string', // primary key
    title: {type: 'string', indexed: true}, // index
    price: 'int',
  },
};

シンプルなサンプル

まずはシンプルなサンプルから入ります。

Hooks を使い、Hooks(useEffect)内で非同期関数(simpleSample)を定義し、Hooks の終わりの直前で非同期関数を呼び出しています。

なお、この記述方法は最新の eslint で警告が出るようになったので、Hooks ではなく、React Native のコンポーネントから呼び出すようにした方がよさそうです。Realmの使用方法自体は問題ありません。(2020/07/26追記)

また、React Native の Realm はデータベースをオープンしたら、自分で必ずクローズする必要があります。

そのため、let realm: Realm;try-catchの前で定義して、finally句でクローズしています。

処理内容は、テーブルに1件データを挿入して、その件数をカウントします。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const simpleSample = async () => {
      printSystemName();
      console.log('Start SimpleSample');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [{name: 'Dog', properties: {name: 'string'}}],
        });

        realm.write(() => {
          realm.create('Dog', {name: 'Rex'});
        });

        // Dog テーブルのデータ件数を取得する
        const info = realm
          ? 'Number of dogs in this Realm: ' + realm.objects('Dog').length
          : 'Nothing...';

        console.log(info);

        // Realm データベースのパスを出力する
        console.log(realm.path);
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

    simpleSample();

  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

なお、iOS シミュレータ、Android エミュレータの実行方法は以下の通りです。

# iOS
$ npx react-native run-ios

# Android
$ npx react-native run-android

データの追加

Person テーブルに1件データを追加し、その後で件数をカウントするサンプルです。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const addData = async () => {
      printSystemName();
      console.log('Start addData');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [PersonSchema],
        });

        realm.write(() => {
          realm.create('Person', {name: '山田太郎', age: 23});
        });

        // Person テーブルのデータ件数を取得する
        const info = realm
          ? 'Number of person in this Realm: ' + realm.objects('Person').length
          : 'Nothing...';

        console.log(info);
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

  addData();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

データの検索

Person テーブルに3件データを追加して、年齡が20歳より大きいものを抽出して、取得データの内容をログに出力しています。

抽出に使用できる演算子は以下を参考にしてください。

realm.io

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const searchData = async () => {
      printSystemName();
      console.log('Start searchData');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [PersonSchema],
        });

        realm.write(() => {
          realm.deleteAll(); // 全件削除
          realm.create('Person', {name: '山田太郎', age: 23});
          realm.create('Person', {name: '佐藤花子', age: 18});
          realm.create('Person', {name: '田中哲朗', age: 33});
        });

        // Person テーブルを検索する
        const people = realm.objects('Person').filtered('age > 20');

        people.forEach(person => {
          // @ts-ignore
          console.log(`name=${person.name}, age=${person.age}`);
        });
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

     searchData();

   // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

データの更新

Person テーブルを検索したデータ1件に対して更新を行い、再度 Person テーブルを全件検索してデータ内容を表示して更新内容を確認しています。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

   const updateData = async () => {
      printSystemName();
      console.log('Start updateData');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [PersonSchema],
        });

        realm.write(() => {
          realm.deleteAll(); // 全件削除
          realm.create('Person', {name: '山田太郎', age: 23});
          realm.create('Person', {name: '佐藤花子', age: 18});
          realm.create('Person', {name: '田中哲朗', age: 33});
        });

        // Person テーブルを検索する
        const person = realm
          .objects('Person')
          .filtered('name CONTAINS "山田"')[0];

        // 更新はプロパティをセットするだけ
        realm.write(() => {
          // @ts-ignore
          person.age = 10;
        });

        // Person テーブルを再検索する(全件取得)
        const people = realm.objects('Person');

        people.forEach(p => {
          // @ts-ignore
          console.log(`name=${p.name}, age=${p.age}`);
        });
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

     updateData();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

データの削除

Person データを検索した1件を削除し、Person テーブルを全検索してデータが削除されたことを確認しています。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const deleteData = async () => {
      printSystemName();
      console.log('Start deleteData');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [PersonSchema],
        });

        realm.write(() => {
          realm.deleteAll(); // 全件削除
          realm.create('Person', {name: '山田太郎', age: 23});
          realm.create('Person', {name: '佐藤花子', age: 18});
          realm.create('Person', {name: '田中哲朗', age: 33});
        });

        // Person テーブルを検索する
        const person = realm
          .objects('Person')
          .filtered('name CONTAINS "山田"')[0];

        // 削除処理
        realm.write(() => {
          realm.delete(person);
        });

        // Person テーブルを再検索する(全件取得)
        const people = realm.objects('Person');

        people.forEach(p => {
          // @ts-ignore
          console.log(`name=${p.name}, age=${p.age}`);
        });
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

   deleteData();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

トランザクション制御を行う

Person テーブルに3件データを追加する処理にトランザクション制御をかけて、処理が成功したらコミット、失敗したらロールバックするサンプルです。

useTransaction(false);で関数を呼び出すと処理が成功、useTransaction(true);で関数を呼び出すと処理が失敗します。

なお、後述しますが、通常はこのサンプルのようなトランザクション制御はあまりしないと思います。行うとすれば、複数テーブルや処理をまたがって更新する場合に限定されると思います。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const useTransaction = async (isException: boolean) => {
      printSystemName();
      console.log('Start useTransaction');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [PersonSchema],
        });

        realm.write(() => {
          realm.deleteAll(); // 全件削除
        });

        try {
          // Start transaction
          realm.beginTransaction();

          realm.create('Person', {name: '山田太郎', age: 23});
          realm.create('Person', {name: '佐藤花子', age: 18});
          realm.create('Person', {name: '田中哲朗', age: 33});

          if (isException) {
            throw new Error('transaction failed.');
          }

          // commit
          realm.commitTransaction();
        } catch (error) {
          console.log(error);

          // rollback
          realm.cancelTransaction();
        }

        // Person テーブルを再検索する(全件取得)
        const people = realm.objects('Person');

        // 件数をカウント
        console.log(`count = ${people.length}`);

        people.forEach(p => {
          // @ts-ignore
          console.log(`name=${p.name}, age=${p.age}`);
        });
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

     useTransaction(false); // commit
     useTransaction(true); // rollback

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

先ほど記述しましたが、通常は以下のようにトランザクション制御をします。

具体的にどういうことかというと、realm.writeブロックの中ではトランザクション制御が行われていて、処理が失敗したら自動的にロールバックされます。ですので、realm.writeブロックとtry-catchを使用していれば、一般的なトランザクション制御は十分かと思います。

try {
  realm.write(() => {
    realm.create('Car', {make: 'Honda', model: 'Accord', drive: 'awd'});
  });
} catch (e) {
  console.log("Error on creation");
}

公式サイトの原文では、トランザクションについて以下のように記載されているので確認してみてください。

Changes to objects in a Realm—creating, updating and deleting—must take place within a write() transaction block. Note that write transactions have a non-negligible overhead; you should try to minimize the number of write blocks within your code.

Note that any exceptions thrown in write() will cancel the transaction. The try/catch block won’t be shown in all examples, but it’s good practice.

https://realm.io/docs/javascript/latest/#writes

主キーとインデックスを使用したサンプル

Book テーブルで、主キーとインデックスを定義しているので、そのまま使用します。

但し、主キーは Realm に AUTOINCREMENT がないので、uuid を使用して主キーが一意になるようにしています。

const App = () => {
  useEffect(() => {
    const printSystemName = () => {
      // OS 名を出力する
      const systemName = DeviceInfo.getSystemName();
      console.log(systemName);
    };

    const usePrimaryKeyAndIndex = async () => {
      printSystemName();
      console.log('Start usePrimaryKeyAndIndex');

      let realm: Realm;

      try {
        realm = await Realm.open({
          schema: [BookSchema],
        });

        realm.write(() => {
          realm.deleteAll(); // 全件削除
          realm.create('Book', {
            id: uuid(),
            title: 'ドメイン駆動設計',
            price: 3000,
          });
          realm.create('Book', {
            id: uuid(),
            title: 'React Native 入門',
            price: 2000,
          });
          realm.create('Book', {
            id: uuid(),
            title: 'Kotlin 入門',
            price: 1000,
          });
        });

        // Book テーブルを検索する
        const books = realm.objects('Book').filtered('price > 1000');

        books.forEach(book => {
          console.log(
            // @ts-ignore
            `id=${book.id}, title=${book.title}, price=${book.price}`,
          );
        });
      } catch (error) {
        console.log(error);
      } finally {
        // Realm データベースは使用後必ずクローズする
        // @ts-ignore
        if (realm !== undefined && !realm.isClosed) {
          realm.close();
        }
      }
    };

     usePrimaryKeyAndIndex();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <SafeAreaView>
        <Text>Hello</Text>
      </SafeAreaView>
    </>
  );
};

export default App;

おわりに

ざっと React Native で Realm を使用する基本的な方法をご紹介しました。

まだ他にも使用方法はあるので、足りない部分については公式サイトをご確認ください。

realm.io

Realm は SQLite よりもパフォーマンスがかなりよいようですし、素の SQL を使用しなくてすむのがうれしいですね。

Expo では Realm を使用できなかったので、それが理由で React Native を採用することが多いかもしれません。