Docs Menu

チュートリアル: React Nativeアプリケーションの PowerSync への移行

年 9 月2024 日現在、Atlas Device SDK(Realm)、 Device Sync、App Services は非推奨になりました。つまり、これらのサービスのユーザーは 9 月 2025 までに別のソリューションに移行する必要があります。 さらに時間が必要な場合は、 サポートにお問い合わせください 。

PowerSync は、 Atlas Device Syncの上位代替手段です。 これは SQLite ベースのソリューションであり、 Device Sync を使用しているモバイルアプリケーションがある場合は移行するのに適したソリューションになる可能性があります。

このチュートリアルでは、 React Nativeで記述されたDevice Syncモバイルアプリケーション をPowerSync に移行するために必要な手順についてガイド。 バックエンドデータは Atlas に保持されるため、PowerSync サービスの構成、ローカルデータベースのスキーマとバインディングの更新、Atlas への書き込み用のバックエンドサービスを設定する必要があります。

このチュートリアルでは、 邦土 Powersync例リポジトリで利用可能なReact NativeのRealm Todo リストアプリケーションを使用します。2

まず、Atlas クラスターをデプロイしてテスト データを入力する必要があります。 これは、Atlas を初めて設定する場合と同様にガイドます。 すでに配置されているクラスターは、自由にスキップしてください。

  1. MongoDB Atlasに移動し、Atlas アカウントに登録するか、すでにアカウントがある場合はサインインします。

  2. 次に、クラスターを作成します。

    Screenshot of the UI

    テスト目的では、デフォルト設定の M0(無料)クラスターを選択します。 ニーズに合わせて追加の変更を行ってください。

  3. [ 配置の作成 ] をクリックします。

    Screenshot of the UI

    ダッシュボードに返されます。 [ クラスターへの接続 ] モーダルが自動的に表示されます。

  4. [ 接続方法の選択 ] をクリックし、[ ドライバー ] を選択します。

    Screenshot of the UI

    この画面から、ステップ 3 に表示されるURLをコピーします。

    Screenshot of the UI

    接続文字列をアプリケーションコードに追加します。これは接続文字列です。 MongoDBインスタンスにアクセスするには必要です。 将来の参照のために接続文字列を保存します。

    次の手順では、PowerSyncインスタンスがデータベースに接続するために使用するユーザー名とパスワードを作成します。

  5. [ Done ] をクリックしてモーダルを閉じます。

    クラスターのデプロイが完了すると、 ダッシュボードは次のようになります。

  6. 新しいデータベースを作成するには、 [データの追加] をクリックします。

    Screenshot of the UI

    [ Atlas でのデータベースの作成 ] カードから、 [ START ] をクリックします。

    Screenshot of the UI

    PowerSync というデータベースと、Item というコレクションを作成し、[データベースの作成] をクリックします。

    Screenshot of the UI

    ダッシュボードに返され、新しく作成されたデータベースとコレクションが表示されます。

    Screenshot of the UI

    最後に、PowerSync がこのデータベースに接続するために使用する新しいユーザーを作成する必要があります。

    左側のサイドバーで、[ セキュリティ ] 見出しの下の [ データベース アクセス ] をクリックします。

    Screenshot of the UI

    [ 新しいデータベースユーザーの追加 ] をクリックし、 という新しいユーザーを作成し、パスワードを入力します。powersync先ほどコピーした接続文字列で使用するユーザー名とパスワードに注意してください。

    注意

    ユーザー名またはパスワードに次の特殊文字のいずれかが含まれている場合は、接続文字列のURLセーフな形式に変換する必要があります 。$:/?!#[]@これは手動で行うことも、urlcoder.org などのURLエンコードアプリケーションを使用することもできます。

    [ データベースユーザー特権 ] セクションで、[ 特定の特権の追加readWrite ]dbAdmin をクリックし、 PowerSyncデータベースの ロールと ロールの特権を追加します。

    Screenshot of the UI

    [ Add User ] をクリックします。

    必要なデータベース権限を持つ新しく作成されたユーザーが表示されます。

    Screenshot of the UI

ユーザー権限の詳細については、 PowerSync ソース データベース セットアップガイドの「MongoDB 」セクションを参照してください。

PowerSync が Atlas で実行中データベースにアクセスするには、 IP アクセス リストにサービスIPアドレスを追加する必要があります。 これらのIPアドレスは、 PowerSync のセキュリティとIPフィルタリングのドキュメントに記載されています。

左側のサイドバーで、[ セキュリティ ] 見出しの下の [ ネットワーク アクセス ] をクリックします。

[+ IPアドレスの追加] をクリックし、 IPアドレスを入力します。将来このリストを管理するユーザーをより支援するために、オプションのコメントとして PowerSync を入力することも推奨します。

[Confirm(確認)] をクリックし、各IPに対して を繰り返します。

まだ行っていない場合は、データベースユーザーのユーザー名とパスワードを使用して、以前にコピーした接続文字列のプレースホルダーを更新します。

この手順では、後の手順でデータを同期するために使用されるサンプルデータをインポートします。

まず、 MongoDB Database Tools をインストールして mongoimport にアクセスできるようにします。 詳しくは、ご利用中のオペレーティング システム用のインストールガイドを参照してください。

database-tools をインストールした後、ターミナルに以下を入力して、mongoimport にアクセスできることを確認します。

mongoimport --version

これにより、ツールのバージョンが返されます。 問題が発生した場合は、上記の インストールガイドを参照してください。

次に、次の内容で sample.json というJSONファイルを作成します。

[
{
"isComplete": false,
"summary": "Complete project documentation",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Buy groceries",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Schedule dentist appointment",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Prepare presentation for next week",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Pay utility bills",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Fix bug in login system",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Call mom",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Submit expense reports",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Plan team building event",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Review pull requests",
"owner_id": "mockUserId2"
}
]

このサンプルデータには、いくつかの To Do リスト項目が含まれています。 owner_id は、このチュートリアルの後半のフィルタリング例に使用されます。

このJSON をインポートするには、次のコマンドを入力して、<connection-string> プレースホルダーを接続文字列に置き換えます。

mongoimport --uri="<connection-string>" --db=PowerSync --collection=Item
--file=sample.json --jsonArray

次のメッセージが表示されます。

10 document(s) imported successfully. 0 document(s) failed to import.

そうでない場合は、コマンド パラメータ(接続文字列を含む)が正しいこと、および Atlas ユーザーが正しいデータベースアクセス権を持っていることを確認します。

挿入されたドキュメントは、 Atlas UIでコレクションに移動するか、 MongoDB Compassビジュアル デスクトップアプリケーションを使用して表示および管理できます。 MongoDB Compassを通じてデータベースとコレクションを表示および管理するには、同じ接続文字列 を使用して接続する必要があります。

Screenshot of the UI

ここで、PowerSync に移動し、登録またはサインインします。

初めてサインインする場合は、開始するために新しいインスタンスを作成する必要があります。

TodoList という新しいインスタンスを作成します。

Screenshot of the UI

接続データベースとしてMongoDB を選択します。

Screenshot of the UI

Atlas接続文字列を使用して接続設定に入力します。

重要

ユーザー名、パスワード、またはその他のURLパラメーターを含まない、接続文字列の短縮バージョンを使用します。 例、接続は mongodb+srv://m0cluster.h6folge.mongodb.net/ のようになります。

前の手順でこのアカウントに割り当てたデータベース名(「PowerSync」)、ユーザー名(「Powersync」)、およびパスワードを入力します。

Screenshot of the UI

[ 接続をテストする ] をクリックして、正常に接続されることを確認します。

次のエラーが表示された場合は、必要な PowerSync サービスの IP がすべて Atlas IP アクセス リストにあることを確認してください。

Screenshot of the UI

まだ問題が発生している場合は、「 MongoDB接続用の PowerSync データベース接続ガイド 」を参照してください。

[ Next(次へ) ] をクリックして、新しい PowerSyncインスタンスを配置します。これが完了するまでに数分かかる場合があります。

インスタンスが配置された後、いくつかの基本的な同期ルールを作成することで、移行されたデータを表示できるようになります。

まず、デフォルトの同期ルールを削除し、以下のように置き換えます。

bucket_definitions:
user_buckets:
parameters: SELECT request.user_id() as user_id
data:
- SELECT _id as id, * FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id

PowerSync サービスに正しく同期するアイテムについては、次の点に注意してください。

  • _idid にマッピングする必要があります。

  • コレクション名 "Item" は引用符で囲む必要があります。 これは、コレクション名が大文字で始まるためです。

  • ユーザー固有のバケットは、データベース全体へのアクセスを提供する globaluser_id に一致する必要があります。 それ以外の場合は、認証トークンから検索される、指定された user_id と照合します。

PowerSync 同期ルールは非常に深いトピックであることに注意してください。 詳細については、この 同期ルール ブログ記事 または PowerSync 同期ルール ドキュメント を参照してください。

[ Save and Deploy(保存と配置) ] をクリックします。また、配置が完了するまでにかなりの時間がかかる場合があります。

配置が完了すると、次のものが表示されます。

Screenshot of the UI

配置が完了すると、適切なステータスが表示されます。

PowerSyncエラーが発生した場合は、 ユーザーが PowerSync ソース データベース設定 のドキュメントに記載されている権限で設定されていることを確認してください。

[ インスタンスの管理 ] をクリックして、同期ルールと配置ステータスを確認します。

この設定を完了するには、 PowerSync 診断アプリを使用して、同期ルールに追加して追加した To Do リストの項目を表示します。 このツールを使用するには、まず開発トークンを作成する必要があります。

  • PowerSync ページの上部にある [Manage Instances] をクリックします。

  • 左側のサイドバーで、TodoList の横にある省略記号(...)をクリックして、このインスタンスのコンテキスト メニューを開き、[インスタンスの編集] を選択します。

  • [ クライアント認証 ]タブを選択し、[ 開発トークンを有効にする ] をクリックします。

  • [ Save(保存) ] をクリックして を配置します。

Screenshot of the UI

TodoList の横にある省略記号(...)をクリックして、このインスタンスのコンテキスト メニューを再度開き、 [開発トークンの生成] を選択します。

トークンの subject/user_id を指定するよう求められます。 これは user_id として機能し、これを実行するように同期ルールを設定できます。

以前に定義した同期ルールでは、 subject/user_id を global に設定して、データセット全体へのアクセス権を持つトークンを生成できます。 これを mockUserId または mockUserId2 に設定して、特定の owner_id で同期することもできます。

生成されたトークンをコピーし、診断アプリを開き、開発トークンを貼り付けます。

注意

開発トークンは 12 時間で期限切れになります。 診断ツールの有効期限が切れると Atlas との同期が停止するため、同期を再開するには新しいトークンを生成する必要があります。

このページのようなページが表示されます。

Screenshot of the UI

左側のサイドバーで[ SQL Console ] をクリックします。

すべてのアイテムを表示するには、SELECT クエリを作成します。

SELECT * FROM Item
Screenshot of the UI

これで、 MongoDBデータベース をモバイルアプリケーションに同期するために必要なサービスがすべてできました。

このフェーズでは、 React NativeのRealm Todo リストアプリケーションをクローンします。 例リポジトリのmain ブランチには、移行の最終結果が含まれています。

例リポジトリを使用してこのガイドに従うには、00-Start-Here ブランチをチェックアウトしてください。

git clone https://github.com/takameyer/realm2powersync
cd realm2powersync
git checkout 00-Start-Here

次に、依存関係をインストールして、エディターがすべてのインポートを選択できるようにし、このプロジェクトを編集する際にエラーが発生しないようにします。

npm install

アプリケーションでは、アクティブなDevice Syncサービスを持つ Atlas クラスターが存在することが前提とされているため、まだ実行できません。 次の手順では、プロジェクトをローカル専用アプリケーションとして実行するために必要な変更を加えます。

Atlas Device Sync部分は、アプリケーションがローカルのみのデータで実行中ようにする必要があります。

まず、source/AppWrapper.txs を開き、AppProviderUserProvidersync の構成を削除します。

更新された AppWrapper.txsファイルは次のようになります。

import React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import { RealmProvider } from '@realm/react';
import { App } from './App';
import { Item } from './ItemSchema';
const LoadingIndicator = () => {
return (
<View style={styles.activityContainer}>
<ActivityIndicator size="large" />
</View>
);
};
export const AppWrapper = () => {
return (
<RealmProvider schema={[Item]} fallback={LoadingIndicator}>
<App />
</RealmProvider>
);
};
const styles = StyleSheet.create({
activityContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-around',
padding: 10,
},
});

次に、source/App.tsx を開き、dataExplorerLink に関する部分と OfflineModeLogout のヘッダー ボタンを削除します(これは後で実装されます)。

更新された App.tsxファイルは次のようになります。

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { LogoutButton } from './LogoutButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <LogoutButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
//headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

最後に、source/ItemListView.tsx を開き、次の更新を行います。

  • フレキシブルな同期サブスクライブコードを削除する

  • ユーザーをモックユーザーに置き換え: - const user={ id: 'mockUserId' };

  • dataExplorerer 参照をすべて削除

  • Show All Tasks スイッチの機能を排除します(これは後で実装されます)

更新された ItemListView.tsxファイルは次のようになります。

import React, { useCallback, useState, useEffect } from 'react';
import { BSON } from 'realm';
import { useRealm, useQuery } from '@realm/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Alert, FlatList, StyleSheet, Switch, Text, View } from 'react-native';
import { Button, Overlay, ListItem } from '@rneui/base';
import { CreateToDoPrompt } from './CreateToDoPrompt';
import { Item } from './ItemSchema';
import { colors } from './Colors';
export function ItemListView() {
const realm = useRealm();
const items = useQuery(Item).sorted('_id');
const user = { id: 'mockUserId' };
const [showNewItemOverlay, setShowNewItemOverlay] = useState(false);
const [showAllItems, setShowAllItems] = useState(true);
// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
({ summary }: { summary: string }) => {
// if the realm exists, create an Item
realm.write(() => {
return new Item(realm, {
summary,
owner_id: user?.id,
});
});
},
[realm, user],
);
// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and delete it
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't delete someone else's task!");
} else {
realm.write(() => {
realm.delete(item);
});
}
}
},
[realm, user],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and update it's 'isCompleted' field
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't modify someone else's task!");
} else {
realm.write(() => {
item.isComplete = !item.isComplete;
});
}
}
},
[realm, user],
);
return (
<SafeAreaProvider>
<View style={styles.viewWrapper}>
<View style={styles.toggleRow}>
<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{ true: '#00ED64' }}
onValueChange={() => {
setShowAllItems(!showAllItems);
}}
value={showAllItems}
/>
</View>
<Overlay
isVisible={showNewItemOverlay}
overlayStyle={styles.overlay}
onBackdropPress={() => setShowNewItemOverlay(false)}>
<CreateToDoPrompt
onSubmit={({ summary }) => {
setShowNewItemOverlay(false);
createItem({ summary });
}}
/>
</Overlay>
<FlatList
keyExtractor={item => item._id.toString()}
data={items}
renderItem={({ item }) => (
<ListItem key={`${item._id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
{!item.isComplete && (
<Button
title="Mark done"
type="clear"
onPress={() => toggleItemIsComplete(item._id)}
/>
)}
<Button
title="Delete"
type="clear"
onPress={() => deleteItem(item._id)}
/>
</ListItem.Content>
</ListItem>
)}
/>
<Button
title="Add To-Do"
buttonStyle={styles.addToDoButton}
onPress={() => setShowNewItemOverlay(true)}
/>
</View>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
viewWrapper: {
flex: 1,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
addToDoButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
completeButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
showCompletedButton: {
borderRadius: 4,
margin: 5,
},
showCompletedIcon: {
marginRight: 5,
},
itemTitle: {
flex: 1,
},
itemSubtitle: {
color: '#979797',
flex: 1,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
},
toggleText: {
flex: 1,
fontSize: 16,
},
overlay: {
backgroundColor: 'white',
},
status: {
width: 40,
height: 40,
justifyContent: 'center',
borderRadius: 5,
borderWidth: 1,
borderColor: '#d3d3d3',
backgroundColor: '#ffffff',
alignSelf: 'flex-end',
},
delete: {
alignSelf: 'flex-end',
width: 65,
marginHorizontal: 12,
},
statusCompleted: {
borderColor: colors.purple,
},
statusIcon: {
textAlign: 'center',
fontSize: 17,
color: colors.purple,
},
});

これらの変更により、アプリはローカルデータベースに対して動作するようになります。

移行を開始する前に、更新されたアプリケーションをビルドして実行し、意図したとおりに動作することを確認する必要があります。

iOSの場合は、次のコマンドを実行します。

npx pod-install
npm run ios

Android の場合は、次のコマンドを実行します。

npm run android

ビルド エラーはすべて、このドキュメントの範囲外であることに注意してください。 ビルド関連の問題が発生している場合は、 React Native のドキュメントを参照して環境が正しく設定されていることを確認してください。

アプリがを実行中間に、基本的な機能を確認できます。 次のことができる必要があります。

  • 新しい項目の作成

  • 項目を完了としてマークします

  • アイテムを削除

Screenshot of the UI

ローカル専用のRealmアプリケーションで を実行中できたら、このアプリケーションを変換して、 PowerSyncクライアントのローカル専用バージョンを使用できます。

PowerSync は SQLite ベースのデータベースを使用するため、互換性を確保するためにスキーマにいくつか変更を加える必要があります。

これを実現するには、 PowerSyncクライアント を設定する必要があります。 詳しくは、 @Powersync/react-native npmリポジトリまたは PowerSync React Nativeセットアップのドキュメント を参照してください。

まず、次のコマンドを実行して、PowerSync React Nativeクライアント、バッキング SQLiteデータベース、非同期イテレータ ポリゴン(手順ごとに必要)、および bson 依存関係(挿入用の ObjectId`` の生成に使用)の依存関係を追加します。 MongoDBへのドキュメント):

npm install @powersync/react-native @journeyapps/react-native-quick-sqlite @azure/core-asynciterator-polyfill bson

ポリゴンを設定するには、index.js を開き、ファイルの先頭に import '@azure/core-asynciterator-polyfill'; を追加します。

更新された index.jsファイルは次のようになります。

import '@azure/core-asynciterator-polyfill';
import 'react-native-get-random-values';
import {AppRegistry} from 'react-native';
import {AppWrapper} from './source/AppWrapper';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => AppWrapper);

依存関係が追加されたので、アプリケーションを再構築する必要があります。

  • iOSの場合は、pod-install を実行します。

  • Android の場合、react-native-quick-sqlite と互換性を持たせるために、必要な最小 SDK を 24 に更新します。 そのためには、android/build.gradle を開き、minSdkVersion を 21 から 24 に変更します。

ここで、 ローカルデータベースのデータ型とスキーマを設定します。

特定のスキーマを設定する方法については、 PowerSync MongoDBタイプ マッピング のドキュメントを参照してください。以下は、使用可能なタイプのクイック参照です。

タイプ
説明

null

未定義、または設定されていない値

integer

64 ビットの符号付き整数

real

64 ビットの浮動点数

text

UTF-8 テキスト string

blob

バイナリ データ

このチュートリアルでは、source/ItemSchema.tsx を次のように変更します。

import {column, Schema, Table} from '@powersync/react-native';
export const ItemSchema = new Table({
isComplete: column.integer,
summary: column.text,
owner_id: column.text,
});
export const AppSchema = new Schema({
Item: ItemSchema,
});
export type Database = (typeof AppSchema)['types'];
export type Item = Database['Item'];

重要

Schema に渡されるプロパティ名は、ローカル テーブルとMongoDBコレクションの名前を表します。 この場合は、名前が Item であることを確認してください。

このコードでは、型を手動で定義する代わりに、AppSchema から直接エクスポートすることに注意してください。

PowerSync にアクセスしてデータをバインドするには、 PowerSyncクライアントのフックとプロバイダーにアクセスする必要があります。この機能は、 PowerSyncContext コンポーネントを通じて提供されます。

まず、source/AppWrapper.tsx を更新して PowerSyncContext を使用し、 PowerSyncクライアントを初期化します。

import React from 'react';
import {App} from './App';
import {AppSchema} from './ItemSchema';
import {PowerSyncContext, PowerSyncDatabase} from '@powersync/react-native';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

次に、PowerSyncクライアント を使用するように ItemListView.tsx を更新します。 これを実現するには、このコンポーネントの上部で使用されているフックをアップデートする必要があります。

  • 書き込みや更新を行うためにローカルデータベースにアクセスするには、usePowerSync フックを使用します。

  • 更新時に自動的に再レンダリングされる Todo リスト項目の一覧を取得するには、useQuery フックを使用します。

次の変更を行います。

  • import { BSON } from 'realm'; を削除

  • Add import { ObjectId } from 'bson';

  • ItemListView 関数の最初の 2 行を次のように変更します。

    export function ItemListView() {
    const db = usePowerSync();
    const {data: items} = useQuery<Item>('SELECT * FROM Item');

次に、createItemdeleteItemtoggleItemIsComplete のメソッドを更新する必要があります。

これらのメソッドのそれぞれで、usePowerSync から返された dbオブジェクトを使用します。 Realmと同様に、ローカルデータベースはトランザクションを開き、挿入、更新、削除などの可変操作を実行します。 また、アプリケーションのフロントエンドにエラーを伝達するために、try/catch ブロックも追加します。

各アイテムの一意の ID を作成するために、コードは bson から ObjectId をインポートしていることに注意してください。 PowerSync では、プライマリキーアイテムの名前は id であることが想定されていることに注意してください。

作成コードは、このロジックに直接項目のデフォルト値も実装します。 この場合、isComplete は false に初期化され、id は新しく作成された ObjectId の string 結果で初期化されます。

createItem メソッドは次のように実装できます。

// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
async ({summary}: {summary: string}) => {
try {
// start a write transaction to insert the new Item
db.writeTransaction(async tx => {
await tx.execute(
'INSERT INTO Item (id, summary, owner_id, isComplete) VALUES (?, ?, ?, ?)',
[new ObjectId().toHexString(), summary, user?.id, false],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

deleteItem メソッドと toggleItemIsComplete メソッドは似ているため、次のように実装します。

// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
async (id: String) => {
// start a write transaction to delete the Item
try {
db.writeTransaction(async tx => {
await tx.execute('DELETE FROM Item WHERE id = ?', [id]);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
async (id: String) => {
// start a write transaction to update the Item
try {
db.writeTransaction(async tx => {
await tx.execute(
'UPDATE Item SET isComplete = NOT isComplete WHERE id = ?',
[id],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

最後に、レンダリングされた FlatList をアップデートします。 次の操作を行います。

  • _id のインスタンスを id に置き換え

  • FlatListkeyExtractor を更新して、id string を直接使用します。

  • 以前は、データベースはObjectId を返していました。 これは string に変換する必要があります。

アップデートされた FlatList は次のようになります。

<FlatList
keyExtractor={item => item.id}
data={items}
renderItem={({item}) => (
<ListItem key={`${item.id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
<Pressable
accessibilityLabel={`Mark task as ${
item.isComplete ? 'not done' : 'done'
}`}
onPress={() => toggleItemIsComplete(item.id)}
style={[
styles.status,
item.isComplete && styles.statusCompleted,
]}>
<Text style={styles.statusIcon}>
{item.isComplete ? '✓' : '○'}
</Text>
</Pressable>
</ListItem.Content>
<ListItem.Content>
<Pressable
accessibilityLabel={'Remove Item'}
onPress={() => deleteItem(item.id)}
style={styles.delete}>
<Text style={[styles.statusIcon, {color: 'blue'}]}>
DELETE
</Text>
</Pressable>
</ListItem.Content>
</ListItem>
)}
/>

コードの更新が完了すると、ローカル PowerSyncクライアント を使用できるようになります。

確認するには、アプリケーションを再ビルドします 。 iOS を使用している場合は、npx pod-install を使用してポッドを更新することを忘れないでください。

Screenshot of the UI

これで、PowerSync を使用して Todo リスト項目を追加、更新、削除できる動作するアプリケーションが完成しました。

問題が発生した場合は、例リポジトリの02 -Migrate-local-Client ブランチでその点までに行われた変更を表示できます。

これで、モバイルアプリケーションがMongoDBからリアルタイムでデータを同期する準備が整いました。

注意

Realmデータがまだ移行されていないことに注意してください。 このガイドでは、 Atlas でホストされているMongoDBクラスターがデータのソースであると想定し、これをアプリケーションに同期します。 ローカル データの移行は、このチュートリアルの範囲外ですが、将来のドキュメントで対処される可能性があります。

これで、PowerSync 診断ツールを使用して検証された、Atlas からの同期されたデータを含む実行中のPowerSync サービスが作成されています。

このフェーズでは、このデータを取得してReact Nativeアプリケーションに同期します。

開始するには、トークンとエンドポイントのいくつかの環境変数を設定する方法を作成する必要があります。

まず、開発依存関係に react-native-dotenv をインストールします。 これは、プロジェクトのルートから .envファイルを受け取り、環境変数をアプリケーションに直接インポートできるようにする Bagel プラグインです。

npm install -D react-native-dotenv

次に、babel.config.jsファイルに次の行を追加します。

module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['module:react-native-dotenv'],
};

types という新しいディレクトリを作成し、その中にインポートする次の変数を含む env.d.ts という名前の新しいファイルを作成します。

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
}

環境変数に必要な値は、PowerSync から取得する必要があります。

  • PowerSync コンソールで、左側のバーで [TodoList] の横にある [] をクリックしてコンテキスト メニューを開きます。

  • [ インスタンスの編集 ] を選択します。

  • URL をコピーして保存します。

Screenshot of the UI

次に、 subject/user_idmockUserId を使用してインスタンスの新しい開発トークンを生成します。生成されたトークンをコピーして保存します。

アプリケーションプロジェクトから、 ルートディレクトリに .envファイルを作成し、作成した PowerSync エンドポイントとトークンを貼り付けます。

POWERSYNC_ENDPOINT=<endpoint>
AUTH_TOKEN=<dev-token>

PowerSyncインスタンスに接続できるように、アプリケーションを若干リファクタリングする必要があります。

まず、sourcePowerSync.ts という新しいファイルを作成し、以下を貼り付けます。

import { AppSchema } from './ItemSchema';
import {
AbstractPowerSyncDatabase,
PowerSyncDatabase,
} from '@powersync/react-native';
import { AUTH_TOKEN, POWERSYNC_ENDPOINT } from '@env';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
class Connector {
async fetchCredentials() {
return {
endpoint: POWERSYNC_ENDPOINT,
token: AUTH_TOKEN,
};
}
async uploadData(database: AbstractPowerSyncDatabase) {
console.log('Uploading data');
}
}
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector);
return powerSync;
};
export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};

このファイルは、次の処理を行います。

  • 新しい Connectorクラスを作成します。このクラスは、 PowerSyncクライアントで開発トークンと PowerSync エンドポイントを設定するために使用されます。

  • モックアウトされた uploadData 関数を定義します。この関数は次のフェーズで変更を Atlas にプッシュするために使用されます。

  • PowerSyncクライアント を設定およびリセットするためのメソッドを定義します。 クライアントをリセットすると、行われた変更はキューに配置されるため、現時点では開発に役立ちます。 これらの変更が処理されるまで、新しい更新は受信されません。

次に、AppWrapper.tsx を更新して新しい setupPowerSync メソッドを使用します。

import { PowerSyncContext } from '@powersync/react-native';
import React from 'react';
import { App } from './App';
import { setupPowerSync } from './PowerSync';
const powerSync = setupPowerSync();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

次に、LogoutButton.tsx をリファクタリングして resetPowerSync メソッドを実装します。 名前を ResetButton.tsx に変更し、内容を次のように更新します。

import React, { useCallback } from 'react';
import { Pressable, Alert, View, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import { resetPowerSync } from './PowerSync';
export function ResetButton() {
const signOut = useCallback(() => {
resetPowerSync();
}, []);
return (
<Pressable
onPress={() => {
Alert.alert('Reset Database?', '', [
{
text: 'Yes, Reset Database',
style: 'destructive',
onPress: () => signOut(),
},
{ text: 'Cancel', style: 'cancel' },
]);
}}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>Reset</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonContainer: {
paddingHorizontal: 12,
},
buttonText: {
fontSize: 16,
color: colors.primary,
},
});

次に、App.tsx を変更して、ヘッダーの左側に Reset ボタンを表示します。

  • import { LogoutButton } from './LogoutButton';import { ResetButton } from './ResetButton'; に置き換え

  • headerLeft で、既存の行を return <ResetButton />; に置き換えます

  • //headerLeft[] ボタンをクリックして、リセット ボタンが表示されるようにします。

変更は、次のようになります。

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ResetButton } from './ResetButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <ResetButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

最後に、react-native-dotenv ライブラリでは、 React Nativeサーバーがキャッシュのクリア でリセットされる必要があります。これは、Bagel に機能を追加する場合では通常のことです。

そのためには、現在実行中のReact Nativeインスタンスを ctrl-c でダウンさせ、次のコマンドを入力して、キャッシュがクリアされたインスタンスを実行します。

npm start -- --reset-cache

これで、Atlas データをReact Nativeアプリケーションに同期する準備がすべて完了しました。

ここで、アプリケーションをリセットします。 以前にアプリケーションのローカルデータベースに変更を加えた場合は、Atlas に保存されている内容の内容でローカルデータベースをリセットするために、新しい Reset ボタンをクリックする必要があります。

これで、mockUserId のすべての Todo リスト アイテムが表示されます。

Screenshot of the UI

問題が発生した場合は、エミュレータまたはシミュレーター内のアプリケーションを削除し、最初から起動するように再ビルドしてください。

まだ問題が発生している場合は、例リポジトリの03 -Sync-Data-From-Atlas ブランチでその点までに行われた変更を表示できます。

データが モバイルアプリケーションに同期されたら、次の手順では、ローカル変更を Atlas に伝達する方法を作成します。

このフェーズでは、次の操作を行います。

  • ConnectoruploadData メソッドを実装します

  • モバイルデバイスからの操作を処理するための簡単なバックエンドサーバーを作成

簡単にするために、このガイドではサーバーをローカルで実行します。 本番環境のユースケースでは、これらのリクエストを処理するためにクラウドサービス(例: AzureApps は、これをサポートするサーバーレスクラウド機能を提供します)。

まず、モバイルアプリケーションでローカル変更が行われたときに uploadData メソッドに送信される操作を確認します。

source/PowerSync.ts に次の変更を加えます。

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
console.log('batch', JSON.stringify(batch, null, 2));
}

次に、モバイルアプリケーションで次のような変更を加えます。

  • アイテムの削除

  • アイテムを完了と不完全の切り替え

  • 新しい項目の追加

uploadData メソッドの実装を完了して、取得リクエストでこの情報を送信します。

まず、.env に新しい値を追加します。

BACKEND_ENDPOINT=http://localhost:8000

および types/env.d.ts:

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
export const BACKEND_ENDPOINT: string;
}

Android エミュレータを使用している場合は、ポート 8000 上の localhost へのリクエストがエミュレータからローカル マシンに転送されることを確認する必要があります。 これを有効にするには、次のコマンドを実行します。

adb reverse tcp:8000 tcp:8000

次に、source/PowerSync.ts のインポート ステートメントに BACKEND_ENDPOINT を追加します。

import { AUTH_TOKEN, POWERSYNC_ENDPOINT, BACKEND_ENDPOINT } from '@env';

次に、uploadData メソッドを更新します。

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
if (batch === null) {
return;
}
const result = await fetch(`${BACKEND_ENDPOINT}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(batch.crud),
});
if (!result.ok) {
throw new Error('Failed to upload data');
}
batch.complete();
}

更新されたメソッドは、 CRUD操作の配列をバックエンドエンドポイントに送信するようになりました。

  • アプリケーションがオフラインの場合、失敗します。

  • アプリケーションが正の応答を受け取った場合、操作は完了としてマークされ、操作のバッチするはモバイルアプリケーションから削除されます。

ここで、プロジェクトに backend という新しいフォルダを作成します。

mkdir backend

次に、package.jsonファイルを作成します。

{
"main": "index.js",
"scripts": {
"start": "node --env-file=.env index.js"
},
"dependencies": {
"express": "^4.21.2",
"mongodb": "^6.12.0"
}
}

この package.json には、.env の変数をサービスに追加する startスクリプトが含まれています。

以前のバージョンの Atlas connection string を使用して新しい .env を作成します。

MONGODB_URI=<connection_string>

ここで、依存関係をインストールします。

npm install

このガイドには、このサービスにTypescriptやその他のツールを追加する方法は含まれていませんが、無料で追加できることに注意してください。 さらに、このガイドでは検証は最小限に抑え、 MongoDBに挿入されるモバイルアプリケーションからのデータを準備するために必要な変更のみを実装します。

まず、次の内容で index.js を作成します。

const express = require("express");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
app.use(express.json());
// MongoDB setup
const client = new MongoClient(
process.env.MONGODB_URI || "mongodb://localhost:27017",
);
// Helper function to coerce isComplete to boolean
function coerceItemData(data) {
if (data && "isComplete" in data) {
data.isComplete = !!Number(data.isComplete);
}
return data;
}
async function start() {
await client.connect();
const db = client.db("PowerSync");
const items = db.collection("Item");
app.post("/update", async (req, res) => {
const operations = req.body;
try {
for (const op of operations) {
console.log(JSON.stringify(op, null, 2));
switch (op.op) {
case "PUT":
await items.insertOne({
...coerceItemData(op.data),
_id: new ObjectId(op.id),
});
break;
case "PATCH":
await items.updateOne(
{ _id: new ObjectId(op.id) },
{ $set: coerceItemData(op.data) },
);
break;
case "DELETE":
await items.deleteOne({
_id: new ObjectId(op.id),
});
break;
}
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(8000, () => {
console.log("Server running on port 8000");
});
}
start().catch(console.error);

上記のサービスでは、isCompleteboolean 値に強制されていることに注意してください。 これにより、新しいトドリスト アイテムは、1または0ではなく、trueまたはfalseでMongoDBに到達します。 ObjectIdインスタンスも op.id から作成されています。 これを _idプロパティに設定すると、データはMongoDB の要件とベストプラクティスに準拠して作成されます。

これで、サーバーを起動できます。

npm start

モバイルアプリケーションはすでにこのエンドポイントに操作を送信しようとしているはずです。 console.log ステートメントにはリクエストが送信され、変更が Atlas に反映されるよう表示されます。

これは、 Atlas UIまたはMongoDB CompassでMongoDBコレクションを表示することで確認できます。

Screenshot of the UI

これで、Atlas との間でデータを同期する完全に機能するモバイルアプリケーションが完成しました。 また、Wifi をオフにして、アプリがオフラインのときにどのように機能するかをテストすることもできます。

問題が発生した場合は、例リポジトリの04 -Write-Backend ブランチでその点までに行われた変更を表示できます。

この最終フェーズでは、2 つのオプションのアプリテスト機能を実装する方法と、プロジェクトの不要なコードと依存関係をクリーンアップする方法について説明します。

このアプリケーション を作成するプロセスでは、次の機能は省略されました。 すべてのタスクを表示 オフラインモードに切り替えます。これらの機能はアプリの機能をテストするのに役立ちますが、本番アプリケーションでの使用を意図したものではありません。

注意

これらの機能に関連する手順は、任意としてマークされています。 不要な場合は、これらの任意の手順をスキップしてください。

オプションの [すべて表示] トグルを実装するために、クライアントパラメーター に基づいてアクティブ化される 2 つ目のバケットが作成されます。これを適用するには、現在の同期セッションを切断し、新しい値セットで再接続します。 この値は view_all と呼ばれるブール値になり、クラスターで作成されたすべての Todo リスト アイテムを表示するための安全でないバックグラウンドとして使用されます。 この機能は、特定のパラメーターに基づいてバケットが動的に作成されることを紹介するのに役立ちます。

注意

ここで使用されている方法は安全でないため、これを実行するにはバケットで accept_potentially_dangerous_queries フラグを有効にする必要があります。 これを安全に実現する方法は、ユーザー ロールに基づいてバッキングデータベース内のユーザーの認可を更新することです。これは、このガイドの範囲外です。

開始するには、PowerSync ダッシュボードに移動し、同期ルールを更新して、view_all パラメータが設定されているバケットを含めます。

bucket_definitions:
user_buckets:
parameters:
- SELECT request.user_id() as user_id
data:
- SELECT _id as id FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id
view_all_bucket:
accept_potentially_dangerous_queries: true
parameters:
- SELECT (request.parameters() ->> 'view_all') as view_all
data:
- SELECT _id as id FROM "Item" WHERE bucket.view_all = true

バケット定義はまとめられているため、view_all_bucket がアクティブな場合は user_buckets データに追加されることに注意してください。

次に、プロジェクト内の source/PowerSync.ts を更新して、view_all フラグの状態を決定するためのローカル変数を含め、それを 接続インスタンスのパラメーターに適用します。

まず、viewAllパラメータを追加し、setupPowerSync 関数を更新します。

let viewAll = false;
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector, {params: {view_all: viewAll}});
return powerSync;
};

次に、次の 2 つの関数を追加します。

export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};
export const toggleViewAll = () => {
viewAll = !viewAll;
resetPowerSync();
};

最後に、source/ItemListView.tsx を更新します。

まず、PowerSync から toggleViewAll をインポートします。

import { toggleViewAll } from './PowerSync';

次に、[すべてのタスクを表示] スイッチの onValueChange 属性を変更して、toggleViewAll メソッドを呼び出します。 次のコードを使用して、Text コンポーネントと Switch コンポーネントを置き換えます。

<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{true: '#00ED64'}}
onValueChange={() => {
setShowAllItems(!showAllItems);
toggleViewAll();
}}
value={showAllItems}
/>

ここでアプリケーションを再起動し、アプリが意図したとおりに動作することを確認します。

Screenshot of the UI

オプションの オフライン モード トグルを実装するには、同期セッションを切断 して再接続する必要があります。 これにより、同期に接続されていないときにローカルな変更を加え、同期セッションが再確立されたときにローカルで変更が送信されたことを確認できます。

接続状態の変数を追加し、これを切り替えるためのメソッドを作成して、PowerSyncクライアントで connect メソッドと disconnect メソッドを呼び出します。

まず、以下を source/PowerSync.ts に追加します。

let connection = true;
export const toggleConnection = () => {
if (connection) {
powerSync.disconnect();
} else {
setupPowerSync();
}
connection = !connection;
};

次に、source/OfflineModeButton.tsx をリファクタリングしてRealm機能を削除し、新しい toggleConnection メソッドを呼び出して置き換えます。 また、いくつかのインポートを追加する必要があります。

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import {toggleConnection} from './PowerSync';
export function OfflineModeButton() {
const [pauseSync, togglePauseSync] = useState(false);
return (
<Pressable
onPress={() => {
toggleConnection();
togglePauseSync(!pauseSync);
}}>
<Text style={styles.buttonText}>
{pauseSync ? 'Enable Sync' : 'Disable Sync'}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonText: {
padding: 12,
color: colors.primary,
},
});

最後に、source/App.tsx を開き、headerRight コンポーネントのコメントを解除してアプリケーションの Stack.Screen に戻します。

<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
headerRight,
}}
/>

ここで、アプリの 2 つ目のインスタンスを開き、いくつかの変更を加えて、更新を確認します。

Screenshot of the UI

最後に、プロジェクトをクリーンアップします 。

次のファイルは安全に削除できます。

  • atlasConfig.json

  • source/WelcomeView.tsx

次の依存関係を package.json から削除することもできます。

  • @realm/react

  • realm

このガイドでは、 PowerSync への移行を開始するためのビルド ブロックについて説明しています。

要約するには、 このガイドに従うことで、次のことが実現されるはずです。

  • サンプルデータを使用してMongoDBデータベースを配置しました

  • サンプルデータを同期する PowerSync サービスを配置しました

  • 診断ツールを使用してこのデータを表示およびクエリする方法を学びます

  • Device Syncモバイルアプリケーションをローカルのみに変換しました

  • ローカル専用のRealmデータベースから PowerSync に移行しました

  • PowerSync からモバイルデータベースへの同期を設定する

  • PowerSyncクライアントからの変更をMongoDBにプッシュするためのバックエンドを作成しました

次の手順では、モバイルアプリケーションの小さな部分を取得し、PowerSync を使用するように変換してみてください。 より高度なユースケースに関する将来のドキュメントにも注意してください。