React Native + Expo のプロジェクトでTypeORMを使えるようにする

TypeORM を使って、 expo-sqlite を操作できます。

スマホアプリ上のデータの保存を SQL を操作するように取り扱えるので、非常に便利です。

必要なライブラリをインストールする

shell
npm install expo-sqlite typeorm reflect-metadata

tsconfig.json の修正

tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}

データベース用のディレクトリとファイルを作成

shell
mkdir -p lib/databases/{entities,repositories}
shell
touch lib/databases/typeorm.config.ts

typeorm.config.ts の設定

lib/databases/typeorm.config.ts
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'expo',
  driver: require('expo-sqlite'),
  database: 'dev-tabmemo',
  logging: [],
  synchronize: true,
  entities: [],
});

export async function initialize() {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
  }
  return AppDataSource.isInitialized;
}

database にはデータベース名を設定します。
開発中はテーブルが色々と呼ばれるので、接頭辞として dev をつけて、リリース前に外してます。

データソースとの接続状態を Recoil で管理する

データベースに接続されていないうちに TypeORM から SQL を投げると、エラーが出ます。
データベースにちゃんと接続できているかを状態として管理します。

Recoil の導入については以下の記事のとおりです。

Recoil を導入後、以下のファイルを作成します。

shell
touch lib/states/atoms/DataSourceReady.ts

DataSourceReady.ts は以下のようにします。

DataSourceReady.ts
import { atom } from 'recoil';

export const DataSourceReady = atom({
  key: 'dataSourceState',
  default: false,
});

最後に App.tsx を以下のように書きます。

App.tsx
import { initialize } from './lib/databases/typeorm.config';
import { DataSourceReady } from './lib/states/atoms/DataSourceReady';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { RecoilRoot, useSetRecoilState } from 'recoil';

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

function DatabaseInitializer() {
  const setDataSourceReady = useSetRecoilState(DataSourceReady);
  useEffect(() => {
    async function initializeDatabase() {
      try {
        const initializeResult = await initialize();

        if (initializeResult) {
          setDataSourceReady(true);
        }
        console.log('[INFO]DataSource initialized');
      } catch (err) {
        console.log(err);
        console.log('[ERROR]DataSource failed to initialize');
        setDataSourceReady(false);
      }
    }

    initializeDatabase();
  }, []);
  return null;
}

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

<DatabaseInitializer /> を上の方に書いて、他のコンポーネントが呼び出される前に呼ぶようにしてください。そうでないと、

EntityMetadataNotFoundError: No metadata for "Tab" was found

というエラーが出てしまう可能性がありmす。 Entity を読み込む前にテーブルにアクセスしてはいけない、ということです。

この状態で、 npx expo start --tunnel を実行して、シミュレーターを立ち上げると、コンソールに以下のように表示されます。

shell
 LOG  [INFO]DataSource initialized

DataSourceReady の使い方

データを取得する際は以下のようにカスタムフックを作成し、 dataSourceReadytrue であることを確認してからデータを fetch すると、事故りにくくなると思います。

useFetchTasks.ts
export const useFetchTasks = () => {
  const [taskItems, setTaskItems] = useState<Task[]>([]);
  const dataSourceReady = useRecoilValue(DataSourceReady);
  const taskReload = useRecoilValue(TaskReload);

  useEffect(() => {
    if (dataSourceReady) {
      const fetch = async () => {
        const tasks = await getTasks();
        setTaskItems(tasks);
      };

      fetch();
    }
  }, [dataSourceReady, taskReload]);

  return { taskItems, setTaskItems };
};

typeorm [EntityMetadataNotFoundError: No metadata for was found.] の原因

typeorm [EntityMetadataNotFoundError: No metadata for was found.] の原因はだいたい2つです。

一つは、 typeorm.config.tsentities に Entity を設定し忘れていること。

typeorm.config.ts
import { Memo, Tab } from './entities/Tab';
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'expo',
  driver: require('expo-sqlite'),
  database: 'dev-tabmemo',
  logging: [],
  synchronize: true,
  entities: [Tab, Memo],
});

export async function initialize() {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
  }
  return AppDataSource.isInitialized;
}

entities に忘れずに、使うエンティティを設定しましょう。

もう1つは、データベースの初期化が完了する前にクエリを投げてしまうこと。これに関しては、 Recoil などでデータベースの状態を監視して、初期化が完了した後にクエリを投げるようにすれば良いです。

sample.tsx
const [tabs, setTabs] = useState<Tab[]>([]);

  const dataSourceReady = useRecoilValue(DataSourceReady);
  const [reloadTabState, setReloadTabState] = useRecoilState(ReloadTabState);

  const fetchTabs = async () => {
    const tabs = await getTabs();
    setTabs(tabs);
  };

  useEffect(() => {
    if (dataSourceReady) {
      fetchTabs().then();
    }
  }, [reloadTabState, dataSourceReady]);