本文書ではmacOS上にReact Nativeの開発用のフレームワークExpoのインストールを行い, React NativeからローカルのSQLiteデータベースの操作方法について確認を行います。React NativeからローカルのSQLiteはexpo-sqliteライブラリだけで操作することができますがさらにサードパーティライブラリのDrizzleを追加することも可能です。前半ではexpo-sqliteライブラリだけを利用した操作、後半ではDrizzleを利用した操作について説明を行なっています。

iOS上での動作確認を行うために手元のiPhoneにExpo Goのインストールを行なっています。利用したexpoのバージョンは51.0.17、macOSのバージョンはsonoma 14.5。

プロジェクトの作成

React Nativeプロジェクトを作成する際はSQLiteの操作のみにスポットを当てているためtemplateオプションでBlank(TypeScript)を選択して行います。アプリケーションの名前は任意の名前をつけることができるのでreact-native-expoと設定しています。


% npx create-expo-app@latest --template
Need to install the following packages:
create-expo-app@3.0.0
Ok to proceed? (y)
✔ Choose a template: › Blank (TypeScript)
✔ What is your app named? … react-native-expo

//略

✅ Your project is ready!

To run your project, navigate to the directory and run one of the following npm commands.

- cd react-native-expo
- npm run android
- npm run ios
- npm run web

プロジェクトの作成が完了するとアプリケーションと同じ名前のディレクトリが作成されるので作成されるディレクトリに移動して開発サーバの起動を行うためにnpm startコマンドを実行します。


% npm start

> react-native-expo@1.0.0 start
> expo start

Starting project at /Users/mac/Desktop/react-native-expo
Starting Metro Bundler

//ここにQRコード表示

› Metro waiting on exp://192.168.2.200:8081
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Using Expo Go
› Press s │ switch to development build

› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web

› Press j │ open debugger
› Press r │ reload app
› Press m │ toggle menu
› Press o │ open project code in your editor

› Press ? │ show all commands

npm startを実行すると以下のエラーが発生したのでHomebrewでwatchmanをインストールすることで問題が解決しました(% brew install watchman)。


Error: EMFILE: too many open files, watch
    at FSEvent.FSWatcher._handle.onchange (node:internal/fs/watchers:207:21)

npm startコマンド実行後に表示されるQRコードを”Expo Go”をインストールしたiPhoneのカメラから撮影してiPhone上でReact Nativeのアプリケーションが起動することを確認します。

画面上に”Open up App.js to start working on your app!”が表示されれば正常に動作しています。”Open up App.js to start working on your app!”の文字列はプロジェクトディレクトリ直下のApp.jsファイルに記述されています。

SQLiteデータベースの設定

ExpoからSQLiteデータベースを操作するためにexpo-sqliteライブラリのインストールを行います。


 % npx expo install expo-sqlite

インストールが完了したらSQLiteデータベースを作成して、作成したデータベースにtestテーブルを作成してデータを挿入します。処理はApp.tsxファイルの中に記述します。


import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import * as SQLite from 'expo-sqlite';
import { useEffect } from 'react';

export default function App() {
  useEffect(() => {
    async function insertData() {
      try {
        const db = await SQLite.openDatabaseAsync('test.db');
        await db.execAsync(`
        PRAGMA journal_mode = WAL;
        CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, intValue INTEGER);
        INSERT INTO test (value, intValue) VALUES ('test1', 123);
        INSERT INTO test (value, intValue) VALUES ('test2', 456);
        INSERT INTO test (value, intValue) VALUES ('test3', 789);
        `);
        const rows = await db.getAllAsync('SELECT * FROM test');
        console.log('rows', rows);
      } catch (error) {
        console.log('Error:', error);
      }
    }
    insertData();
  }, []);

  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

データベースの作成はopenDatabaseAsyncメソッドで行い、引数にはデータベースのファイル名を指定します。SQLiteデータベースはファイルベースのデータベースなのでデータベースの作成と同時にファイルが作成され、ファイルの中にデータが保存されます。openDatabaseAsyncメソッドを実行した際に引数に指定したファイルが存在しない場合のみ指定した名前でファイルが新規作成されます。

execAsyncメソッドでtestテーブルの作成と3件のデータを挿入しています。挿入後getAllAsyncメソッドを利用してtestテーブルのデータを取得しています。

コードが実行されるとnpm startコマンドを実行したコンソールに配列で挿入したデータが表示されます。説明した通りReact Nativeから簡単にローカルのSQLiteデータベースを操作することができます。


 LOG  rows [{"id": 1, "intValue": 123, "value": "test1"}, {"id": 2, "intValue": 456, "value": "test2"}, {"id": 3, "intValue": 789, "value": "test3"}

FlatListによるリスト表示

データベースに挿入したデータを取得するだけではなくFlatListコンポーネントを利用して画面上にSQLiteデータベースから取得したデータを表示させてみましょう。データベースから取得したデータはuseState Hookを利用して保存します。コードを更新して実行さする度にデータが追加されるのでinsertDataの中で”delete from test”でテーブル内のデータを削除しています。


import { StatusBar } from 'expo-status-bar';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import * as SQLite from 'expo-sqlite';
import { useEffect, useState } from 'react';

export default function App() {
  const [data, setData] = useState([]);
  useEffect(() => {
    async function insertData() {
      try {
        const db = await SQLite.openDatabaseAsync('test.db');
        await db.runAsync('DELETE FROM test');
        await db.execAsync(`
        PRAGMA journal_mode = WAL;
        CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, intValue INTEGER);
        INSERT INTO test (value, intValue) VALUES ('test1', 123);
        INSERT INTO test (value, intValue) VALUES ('test2', 456);
        INSERT INTO test (value, intValue) VALUES ('test3', 789);
        `);
        const rows = await db.getAllAsync('SELECT * FROM test');
        setData(rows);
      } catch (error) {
        console.log('Error:', error);
      }
    }
    insertData();
  }, []);

  return (
    <View style={styles.container}>
      {/* <Text>Open up App.js to start working on your app!</Text> */}
      <FlatList
        data={data}
        renderItem={({ item }) => (
          <Text style={styles.item}>
            {item.value}/{item.intValue}
          </Text>
        )}
        keyExtractor={(item) => item.id}
      />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  item: {
    padding: 20,
    fontSize: 18,
  },
});

上記のコードでデータベースに保存したデータを”Expo Go”をインストールしたiPhone上でリスト表示することができます。

データベースファイルはどこに?

React NativeからSQLiteデータベースの操作を行うことができましたがopenDatabaseAsyncメソッドで指定したtest.dbファイルはどこに保存されているのでしょう。

作成されたデータベースファイルはexpo-file-systemライブラリを利用してファイルシステムにアクセスすることで確認することができます。”Expo Go”で動作確認をしている場合はExpo Goが動作しているiPhone上に保存されます。開発を行なっているmacOSのプロジェクトディレクトリの中に作成されるわけではありません。

expo-file-systemライブラリのインストールを行います。」


 % npx expo install expo-file-system

useEffect Hookの中にlistFiles関数を追加してFileSystem.readDirectoryAsyncメソッドを利用してファイルシステムにアクセスを行いファイルまたはディレクトリの情報を取得します。


  useEffect(() => {
    async function listFiles() {
      try {
        const files = await FileSystem.readDirectoryAsync(
          FileSystem.documentDirectory
          // `${FileSystem.documentDirectory}SQLite`
        );
        console.log('Files:', files);
      } catch (error) {
        console.log('Error:', error);
      }
    }

    listFiles();
  }, []);

コードを実行すると”npm start”コマンドを実行したターミナルには”LOG Files: [“SQLite”]”が表示されます。FileSystem.readDirectoryAsyncの戻り値の配列の中にファイル名、ディレクトリ名が保存されます。”SQLite”はディレクトリ名なのでさらにSQLiteディレクトリの下のファイルを確認するためにreadDirectoryAsyncの引数のパスにSQLiteを追加します。


const files = await FileSystem.readDirectoryAsync(
  // FileSystem.documentDirectory
  `${FileSystem.documentDirectory}SQLite`
);

コードを実行するとターミナルにはSQLite.openDatabaseAsyncの引数に設定した”test.db”を表示されます。このようにexpo-file-systemライブラリを利用することでファイルシステム上に作成したデータベースファイルを確認することができます。


Files: ["test.db"]

ファイルを削除したい場合にはSQLite.deleteDatabaseAsyncメソッドの引数に削除したいファイル名を指定することで削除することができます。削除を行なってreadDirectoryAsyncメソッドを実行するとSQLIteディレクトリからtest.dbファイルが削除されていることが確認できます。

Drizzleを利用した場合

expo-sqliteライブラリだけでもローカルのSQLiteデータベースを操作できることがわかりましたが、Drizzleを利用するためにライブラリのインストールを行います。


 % npm i drizzle-orm
 % npm i -D drizzle-kit

既存のテーブルを利用した場合

Drizzleを利用する場合は事前にテーブルのスキーマを定義しておく必要があります。ここで利用するtestテーブルは作成済みなのでApp.jsファイル内でsqliteTableをimportしてtestテーブルのスキーマを定義します。


//略
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

const test = sqliteTable('test', {
  id: integer('id').notNull(),
  value: text('value').notNull(),
  intValue: integer('intValue'),
});
//略

useEffect Hookの中でgetData関数を追加してその関数の中でdrizzleを経由してデータベースにアクセスを行い、既存のtestテーブルからデータを取得します。db.select().from(test)の引数には定義したスキーマを指定しています。


import { StatusBar } from 'expo-status-bar';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite/next';
import { useEffect, useState } from 'react';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

const test = sqliteTable('test', {
  id: integer('id').notNull(),
  value: text('value').notNull(),
  intValue: integer('intValue'),
});

export default function App() {
  const [data, setData] = useState([]);
  useEffect(() => {
    async function getData() {
      try {
        const expo = openDatabaseSync('test.db');
        const db = drizzle(expo);
        const rows = await db.select().from(test);

        setData(rows);
      } catch (error) {
        console.log('Error:', error);
      }
    }

    getData();
  }, []);

  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <FlatList
        data={data}
        renderItem={({ item }) => (
          <Text style={styles.item}>
            {item.value}/{item.intValue}
          </Text>
        )}
        keyExtractor={(item) => item.id.toString()}
      />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  item: {
    padding: 20,
    fontSize: 18,
  },
});

Drizzleを利用する前と同様に”Expo Go”をインストールしたiPhone上にリストが表示されます。

Drizzle Kitを利用したMigration

既存のテーブルを元にスキーマを定義してテーブルにアクセスすることができましたがDrizzleを利用する場合Drizzle KitのMigration機能を利用して定義したスキーマからテーブルを作成します。

React NativeでDrizzle Kitを利用するためには追加ライブラリのインストールと設定ファイルの作成、更新が必要なので設定を行っていきます。ドキュメントを参考に設定しています。

babel用のプラグインのインストールを行います。


% npm install babel-plugin-inline-import

babel.config.jsファイルを更新します。拡張子”.sql”はDrizzle Kitから作成するMigrationファイルのファイル名についている拡張子です。


module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [['inline-import', { extensions: ['.sql'] }]],
  };
};

metro.config.jsとdrizzle.config.tsファイルが存在しない場合は、プロジェクトディレクトリ直下に作成して以下のコードを記述します。


const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push('sql'); // <--- add this
module.exports = config;

drizzle.confg.tsファイルはdrizzleの設定ファイルです。shemaにはスキーマ定義用ファイルを指定します。outにはMigration後に作成されるディレクトリを指定しています。dialectには"sqlite"、driverには"expo"を指定しています。


import type { Config } from 'drizzle-kit';
export default {
  schema: './db/schema.ts',
  out: './drizzle',
  dialect: 'sqlite',
  driver: 'expo', // <--- very important
} satisfies Config;

drizzle.config.tsファイルで設定したスキーマファイルを作成するためにdbディレクトリを作成してその下にschema.tsファイルを作成します。schema.tsファイルにはtodosテーブルのスキーマを定義します。


import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';

export const todos = sqliteTable('todos', {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text('name'),
  isCompleted: integer('isCompleted', { mode: 'boolean' })
    .notNull()
    .default(false),
});

定義したスキーマを利用してテーブルを作成するためのマイグレーションファイルを作成するためにdrizzle-kit generateコマンドを実行します。


 % npx drizzle-kit generate
drizzle-kit: v0.22.8
drizzle-orm: v0.31.2

No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/mac/Desktop/react-native-expo/drizzle.config.ts'
1 tables
todos 3 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0000_curvy_night_thrasher.sql 🚀

コマンドを実行するとdrizzle.config.tsファイルのoutに指定したdrizzleディレクトリが作成されその下にランダムな名前のsqlファイルが作成されます。

sqlファイルの中にはスキーマ定義からテーブルを作成するためのSQLが生成され記述されていることがわかります。


CREATE TABLE `todos` (
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
	`name` text,
	`isCompleted` integer DEFAULT false NOT NULL
);

migrationファイルが作成できたのでmigrationファイルを利用してデータベースファイルとtodosテーブルを作成します。


import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite/next';
import { useMigrations } from 'drizzle-orm/expo-sqlite/migrator';
import migrations from './drizzle/migrations';

const expoDb = openDatabaseSync('db.db');
const db = drizzle(expoDb);

export default function App() {
  const { success, error } = useMigrations(db, migrations);
  if (error) {
    return (
      <View>
        <Text>Migration error: {error.message}</Text>
      </View>
    );
  }
  if (!success) {
    return (
      <View>
        <Text>Migration is in progress...</Text>
      </View>
    );
  }
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

コードを実行するとデータベースファイルとtodosテーブルが作成されますが上記のコードだけでは実際にデータベースファイルとテーブルが作成されたかわかりません。

先ほどと同様にexpo-file-systemライブラリを利用してファイルシステム上に作成されるデータベースファイルを確認することもできますがDrizzleを経由してデータの挿入を行ってみます。


import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite/next';
import { todos } from './db/schema';
const expoDb = openDatabaseSync('db.db');
const db = drizzle(expoDb);

export default function App() {
  const result = db
    .insert(todos)
    .values({ name: 'Learn Drizzle', isCompleted: false })
    .run();
  console.log('result', result);

  const allTodo = db.select().from(todos).all();
  console.log('allTodo', allTodo);

  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

コードを実行すると"npm start"コマンドを実行したターミナルに挿入したデータベースに挿入したデータが表示されます。


 LOG  result {"changes": 1, "lastInsertRowId": 1}
 LOG  allTodo [{"id": 1, "isCompleted": false, "name": "Learn Drizzle"}]

Drizzle Kitを利用してMigrationからテーブルを作成し、Drizzleを利用してデータ挿入、データ取得が行えることが確認できました。

Drizzle Studioによるデータの閲覧

Drizzle Studioを利用することでブラウザ上からデータベースの内容を確認することができます。GUIを利用してデータベースを確認できる非常に便利なツールです。

Drizzle StudioをReact Nativeで利用するため追加のライブラリをインストールします。


 % npm i expo-drizzle-studio-plugin

Drizzle Studioを起動できるようにApp.tsxファイルを更新します。


import { useDrizzleStudio } from 'expo-drizzle-studio-plugin';
import * as SQLite from 'expo-sqlite';
import { View } from 'react-native';

const db = SQLite.openDatabaseSync('db.db');

export default function App() {
  useDrizzleStudio(db);

  return <View></View>;
}

"npm start"コマンドを実行してExpo Goを利用した後にターミナル画面でShift+mを実行するとメニューが表示されるので"Open expo-drizzle-studio-plugin"を選択します。


? Dev tools (native only) › - Use arrow-keys. Return to submit.
❯   Inspect elements
    Toggle performance monitor
    Toggle developer menu
    Reload app
    Open React devtools
    Open expo-drizzle-studio-plugin

ブラウザが起動するとデータベースの中身がGUIで表示されます。

 Drizzle Studioを利用してDBへのアクセス
Drizzle Studioを利用してDBへのアクセス

ここまでの動作確認でReat Native上からローカルのSQLiteデータベースの操作方法を一通り確認することができました。