はじめに
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更新)
node.js のバージョンの切り替えは、nodenv を使用しています。nodenv については以下の記事を参考にしてください。
サンプルコード
この記事で紹介するサンプルコードは、GitHub に上げてありますので必要に応じ参考にしてください。
使用するライブラリ
まず、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 ライセンスになります。
ログに 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追記)
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歳より大きいものを抽出して、取得データの内容をログに出力しています。
抽出に使用できる演算子は以下を参考にしてください。
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.
主キーとインデックスを使用したサンプル
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 は SQLite よりもパフォーマンスがかなりよいようですし、素の SQL を使用しなくてすむのがうれしいですね。
Expo では Realm を使用できなかったので、それが理由で React Native を採用することが多いかもしれません。