はじめに

iOSやAndroidのアプリでは、サーバーAPIから新しいデータを取得する前に、キャッシュしておいたデータをユーザーに対して表示するということはよくあります。アプリが終了してもキャッシュを保持しておきたい場合、対象のデータを端末に保存する(永続化する)必要があります。
今回は、React Nativeでの開発において、データを永続化するためにどういう手段があるのか調べてみました。

React Nativeとデータ永続化

React Nativeでデータを永続化する方法は色々あります。React Native標準の仕組みにはAsyncStorageというものがあり、iOSとAndroidに両方対応しています。

AsyncStorageの他には、Realm、SQLite、Couchbase、MongoDBといった選択肢があります。

awesome-react-native で今日時点でスターが多いライブラリは、realmreact-native-storage (AsyncStorageのラッパー)や react-native-sqlite-storage (SQLiteのラッパー)となっています。

  • react-native-couchbase-lite ★71 - couchbase lite binding for react-native
  • react-native-db-models ★145 - Local DB Models for React Native Apps
  • react-native-level-fs ★12 - fs for react-native using level-filesystem and asyncstorage-down
  • react-native-mongoose ★10 - A AsyncStorage based mongoose like storage for react-native
  • react-native-pouchdb ★30 - Run pouchdb in React Native!
  • react-native-simple-store ★344 - A minimalistic wrapper around React Native's AsyncStorage.
  • react-native-sqlite-storage ★668 - SQLite3 bindings for React Native (Android & iOS)
  • react-native-sqlite ★474 - SQLite3 bindings for React Native
  • react-native-sqlite-2 ★13 - SQLite3 Native Plugin for React Native for both Android and iOS
  • react-native-storage ★715 - This is a local storage wrapper for both react-native(AsyncStorage) and browser(localStorage). ES6/babel is needed.
  • react-native-store ★442 - A simple database base on react-native AsyncStorage.
  • realm ★1755 - An alternative mobile database to SQLite & key-value stores.
  • pouchdb-adapter-react-native-sqlite ★11 - PouchDB adapter using ReactNative SQLite as its backing store
  • react-native-persistent-job ★55 - Run async tasks that retry after a crash, connection loss or exception

AsyncStorage

AsyncStorageは、シンプルな、暗号化されていない、非同期の、永続的な、key-valueストアです。

Androidアプリの場合は、RocksDBかSQLiteのどちらか利用可能な方にデータが保存されます。

iOSアプリの場合は、ネイティブのコードが実行され、シリアライズされたdictionaryとしてデータがファイルに保存されます。React Nativeのコードをざっと読んだところ、ネイティブの writeToFile メソッドでファイルを書き込むという実装になっていました。

コード

公式ドキュメントのコードです。各メソッドではPromiseオブジェクトが返却されます。

try {
  await AsyncStorage.setItem('@MySuperStore:key', 'I like to save it.');
} catch (error) {
  // Error saving data
}
try {
  const value = await AsyncStorage.getItem('@MySuperStore:key');
  if (value !== null){
    // We have data!!
    console.log(value);
  }
} catch (error) {
  // Error retrieving data
}

margeItem()multiMerge()のメソッドを使うと、既に存在するキー・値と、新しい入力の値を統合することができます。

let UID123_object = {
 name: 'Chris',
 age: 30,
 traits: {hair: 'brown', eyes: 'brown'},
};
// You only need to define what will be added or updated
let UID123_delta = {
 age: 31,
 traits: {eyes: 'blue', shoe_size: 10}
};

AsyncStorage.setItem('UID123', JSON.stringify(UID123_object), () => {
  AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta), () => {
    AsyncStorage.getItem('UID123', (err, result) => {
      console.log(result);
    });
  });
});

// Console log result:
// => {'name':'Chris','age':31,'traits':
//    {'shoe_size':10,'hair':'brown','eyes':'blue'}}

Realm

iOSやAndroidのネイティブ開発で採用される例が増えている、モバイルアプリケーション向けデータベースです。

Realm: リアクティブなモバイルアプリを短期間にの記事では、以下がRealmを使うべき理由であると書かれています。

  • 使い方が簡単
  • 速い!
  • クロスプラットフォーム
  • 先進的
  • 信頼性
  • コミュニティドリブン
  • サポート

Realmはデータベースであり、(特にiOSの場合、データをファイルに書き込む)AsyncStorageとは仕組みが異なります。AsyncStorageやそのラッパーを使った場合とは、クエリの使い勝手や速度面で差が出そうです。

なお上記記事のベンチマーク結果では、20万件のレコード(1000件のマッチ)のクエリにおいて、SQLiteよりも速く、AsyncStorageの185倍のパフォーマンスが出ています。

コード

Realm JavaScript 2.0.12 のコードです。

スキーマを定義する必要があります。

ネイティブ開発でRealmを使ったことがある人は、戸惑うことなくコードを書いていけそうです。

const Realm = require('realm');

// Define your models and their properties
const CarSchema = {
  name: 'Car',
  properties: {
    make:  'string',
    model: 'string',
    miles: {type: 'int', default: 0},
  }
};
const PersonSchema = {
  name: 'Person',
  properties: {
    name:     'string',
    birthday: 'date',
    cars:     'Car[]',
    picture:  'data?' // optional property
  }
};

Realm.open({schema: [CarSchema, PersonSchema]})
  .then(realm => {
    // Create Realm objects and write to local storage
    realm.write(() => {
      const myCar = realm.create('Car', {
        make: 'Honda',
        model: 'Civic',
        miles: 1000,
      });
      myCar.miles += 20; // Update a property value
    });

    // Query Realm for all cars with a high mileage
    const cars = realm.objects('Car').filtered('miles > 1000');

    // Will return a Results object with our 1 car
    cars.length // => 1

    // Add another car
    realm.write(() => {
      const myCar = realm.create('Car', {
        make: 'Ford',
        model: 'Focus',
        miles: 2000,
      });
    });

    // Query results are updated in realtime
    cars.length // => 2
  })
  .catch(error => {
    console.log(error);
  });

所感

シンプルな設定値を扱う場合や、検索条件を指定して取得する必要が無いデータを扱う場合は、AsyncStorageだと機能を手軽に実装できます。iOSネイティブ開発でNSUserDefaultsに保存していたようなデータもAsyncStorageで扱えばよいでしょう。

処理速度が重要になる場合や、iOSネイティブ開発においてCoreDataで管理するようなデータを扱う場合は、予めRealmや他のDBを検討しておくと良さそうです。

さいごに

ベンチマークについても書きたかったのですが、今日はここまで。運用についての知見も得られれば、別途書きたいと思います。

参考