tag:crieit.net,2005:https://crieit.net/users/SofPyon/feed SofPyonの投稿 - Crieit CrieitでユーザーSofPyonによる最近の投稿 2021-03-01T21:18:22+09:00 https://crieit.net/users/SofPyon/feed tag:crieit.net,2005:PublicArticle/16708 2021-03-01T21:16:04+09:00 2021-03-01T21:18:22+09:00 https://crieit.net/posts/React-Native-Realm React NativeでローカルデータベースRealmを使ったスマホアプリをつくる <p><a href="https://crieit.now.sh/upload_images/6edea335353bb5281df61e17ca2f1cab603cd2c9beea3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6edea335353bb5281df61e17ca2f1cab603cd2c9beea3.png?mw=700" alt="アイキャッチ" /></a></p> <blockquote> <p>本記事は<a target="_blank" rel="nofollow noopener" href="https://soji.dev/blog/realm-in-react-native">ブログ記事</a>の転載です。</p> </blockquote> <p>ユーザーが作成するデータをスマホにのみ保存し、サーバー上には保存しない React Native アプリを開発する場合、スマホ内のローカルデータベースにデータを保存することになります。本記事では、ローカルデータベースの1つである <a target="_blank" rel="nofollow noopener" href="https://realm.io/">Realm</a> を使った React Native アプリの開発方法を紹介します。</p> <h2 id="なぜサーバーではなくスマホに保存するのか"><a href="#%E3%81%AA%E3%81%9C%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AB%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B">なぜサーバーではなくスマホに保存するのか</a></h2> <p>最近はFirebaseのようなmBaaS (Mobile Backend as a Service) が登場したことで、アプリ開発者がインフラを触るハードルが下がってきています。それでもなおサーバーではなくスマホにデータベースを保存するモチベーションとして、以下が挙げられます。</p> <ul> <li>サーバーの障害によってアプリが使えなくなることがない</li> <li>インターネットが使えない場所でもアプリを利用できる</li> <li>アプリ開発者がサーバーを維持管理する必要がなくなる <ul> <li>FirebaseのようなmBaaSでも、ユーザー増に伴い金銭的な負担が生じる</li> </ul></li> <li>大規模なデータ流出は起こらない <ul> <li>アプリ開発者のもとにユーザーのデータが集まらないため、データ流出の危険性は下がる</li> </ul></li> </ul> <h2 id="使用フレームワーク・ライブラリ"><a href="#%E4%BD%BF%E7%94%A8%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%BB%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">使用フレームワーク・ライブラリ</a></h2> <p>本記事では以下のフレームワーク、ライブラリを利用します。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/">React Native</a> <ul> <li>クロスプラットフォーム対応のスマホアプリを開発できるフレームワーク。</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://realm.io/">Realm</a> <ul> <li>オフラインデータベース。開発元はMongoDB社により2019年買収。MongoDB社が提供する<a target="_blank" rel="nofollow noopener" href="https://www.mongodb.com/realm">MongoDB Realm</a>(mBaaS)を使うことで、複数端末間でデータベースを同期することもできるが、本記事の対象外。</li> </ul></li> </ul> <h2 id="SQLiteとの比較"><a href="#SQLite%E3%81%A8%E3%81%AE%E6%AF%94%E8%BC%83">SQLiteとの比較</a></h2> <p>スマホ内にデータベースを保存する方法として、RealmのほかにSQLiteがあります。RealmとSQLiteをざっくり比較すると、以下のようになります。</p> <ul> <li>RealmはSQLiteより軽い <ul> <li>Realmでは、クエリの結果は遅延読み込みされる</li> </ul></li> <li>RealmではSQLを書く必要がない</li> <li>ただし、RealmはExpoで使えない(SQLiteはExpoで使える)</li> </ul> <h2 id="本記事の前提"><a href="#%E6%9C%AC%E8%A8%98%E4%BA%8B%E3%81%AE%E5%89%8D%E6%8F%90">本記事の前提</a></h2> <p>まだReact Native 開発環境のセットアップをしていない場合、<a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/docs/environment-setup">Setting up the development environment(React Nativeドキュメント)</a>にある "React Native CLI Quickstart" の内容を読み、指示に従ってください。</p> <p>また、本記事の内容は、以下の環境で動作確認しています。</p> <ul> <li>macOS Big Sur 11.2.1</li> <li>React Native 0.63.4</li> <li>Node.js v14.16.0</li> <li>Xcode 12.4</li> <li>Simulator 上の iPhone 11 / iOS 14.4</li> </ul> <h2 id="Realmの準備"><a href="#Realm%E3%81%AE%E6%BA%96%E5%82%99">Realmの準備</a></h2> <h3 id="React Native アプリの作成"><a href="#React+Native+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E4%BD%9C%E6%88%90">React Native アプリの作成</a></h3> <p>ここでは、MyRealmAppという名前のReact Nativeアプリを作成してみます。なお、ExpoではRealmを使うことができませんので注意してください。</p> <pre><code class="bash">$ npx react-native init MyRealmApp </code></pre> <h3 id="Realmのインストール"><a href="#Realm%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Realmのインストール</a></h3> <pre><code class="bash">$ cd MyRealmApp $ npm install realm $ cd ios && pod install && cd .. </code></pre> <p>PCにインストールされているNode.jsのバージョンが古いとエラーになることがあります。</p> <p>もし、上記の<code>npm install realm</code>コマンドを実行した結果、<code>The N-API version of this Node instance is (数値). This module supports N-API version(s) (数値). This Node instance cannot run this module.</code> というエラーが表示された場合、Node.js のバージョンを上げてください。</p> <h2 id="スキーマの定義とRealmの初期化"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%A8Realm%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96">スキーマの定義とRealmの初期化</a></h2> <p>本記事では例としてタスク管理アプリを設計してみます。タスクを表現する<code>Task</code> クラスと、各 Task にはサブタスクを設定できるよう、<code>SubTask</code> クラスを定義します。</p> <p>まず、作成した React Native プロジェクト内に src フォルダを作成します。src フォルダは、 <code>App.js</code> と同階層のフォルダに作成してください。次に、src フォルダ内に以下のファイルを作成します。ファイル名は <code>realm.js</code> とします。</p> <h4 id="./src/realm.js"><a href="#.%2Fsrc%2Frealm.js">./src/realm.js</a></h4> <pre><code class="js">// Taskの定義 const taskSchema = { name: 'Task', primaryKey: '_id', properties: { _id: 'objectId', // 'string' や 'int' でも OK name: 'string', description: 'string?', // ?をつけると optional isDone: 'bool', createdAt: 'date', subTasks: 'SubTask[]', // クラス名 + '[]' で1対多のリレーションを設定できる }, }; // SubTaskの定義 const subTaskSchema = { name: 'SubTask', primaryKey: '_id', properties: { _id: 'objectId', name: 'string', isDone: 'bool', createdAt: 'date', }, }; // Realmの初期化 export const openRealm = () => { const config = { schema: [taskSchema, subTaskSchema], schemaVersion: 1, // スキーマを変更したらインクリメントする(後述) }; return new Realm(config); }; export {BSON} from 'realm'; </code></pre> <h2 id="データベースの操作"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E6%93%8D%E4%BD%9C">データベースの操作</a></h2> <p>React NativeでRealmを使う前に、まずRealm単体でデータベースを操作する方法を見ていきます。</p> <p>このセクションのコードは、作成した React Native プロジェクトのフォルダに入っている <code>App.js</code> 内の適当な場所に書いた上で <code>npx react-native run-ios</code> コマンドを実行すると Simulator 上で動作確認できます。</p> <p>(<code>npx react-native run-ios</code> コマンドがエラーで失敗する場合、<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/66019068/event2-event-config-h-file-not-found">Flipperのバージョンを変更してみてください(Stack Overflow 英語版)</a>)</p> <h3 id="オブジェクトの新規作成"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90">オブジェクトの新規作成</a></h3> <pre><code class="js">import {openRealm, BSON} from './src/realm'; const realm = openRealm(); // 更新系はすべて realm.write(() => { }) (=トランザクション)内に書く realm.write(() => { // サブタスクを作成しない場合 realm.create('Task', { _id: new BSON.ObjectId(), name: 'タスクの名前', isDone: false, createdAt: new Date(), }); // サブタスクを作成する場合 realm.create('Task', { _id: new BSON.ObjectId(), name: 'タスクの名前', isDone: false, createdAt: new Date(), subTasks: [ { _id: new BSON.ObjectId(), name: 'サブタスクの名前', isDone: false, createdAt: new Date(), }, ], }); }); </code></pre> <h3 id="オブジェクトの取得"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%8F%96%E5%BE%97">オブジェクトの取得</a></h3> <p><code>filtered</code> で利用可能なクエリについては<a target="_blank" rel="nofollow noopener" href="https://docs.mongodb.com/realm/react-native/query-engine/">公式ドキュメント</a>を参照してください。</p> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); // タスクを全部取得 const tasks = realm.objects('Task'); console.log(tasks[0].name); // 「タスクの名前」と表示される console.log(tasks[1].subTasks[0].name); // 「サブタスクの名前」と表示される // フィルタの例 — 完了しているタスクのみ取得 const done = tasks.filtered('isDone == true'); console.log(`完了しているタスクは${done.length}件です。`); // ソートの例 — 名前順で取得 const sorted = tasks.sorted('name'); console.log(sorted[0].name); </code></pre> <h3 id="オブジェクトの更新"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%9B%B4%E6%96%B0">オブジェクトの更新</a></h3> <p>JavaScriptのオブジェクトを操作するのと同じ方法でRealm上のデータを更新できます。</p> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); realm.write(() => { // 更新対象のタスク const task = realm.objects('Task')[0]; // 更新 task.name = '新しいタスクの名前'; task.isDone = !task.isDone; }); </code></pre> <h3 id="オブジェクトの削除"><a href="#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%89%8A%E9%99%A4">オブジェクトの削除</a></h3> <pre><code class="js">import {openRealm} from './src/realm'; const realm = openRealm(); realm.write(() => { // 削除対象のタスク const task = realm.objects('Task')[0]; if (task) { // まずサブタスクを削除 realm.delete(task.subTasks); // 削除 realm.delete(task); } }); </code></pre> <h2 id="React NativeでRealmを使う"><a href="#React+Native%E3%81%A7Realm%E3%82%92%E4%BD%BF%E3%81%86">React NativeでRealmを使う</a></h2> <p>React Nativeアプリ内でTaskを操作できるようにするためのコンテキストと React Hook を作ります。</p> <p>以下の例は、GitHubの <a target="_blank" rel="nofollow noopener" href="https://github.com/mongodb-university/realm-tutorial-react-native">mongodb-university/realm-tutorial-react-native</a> リポジトリ内のコードを参考に、本記事用に書き換えたものです。</p> <p>まず<code>src</code> フォルダ内に<code>providers</code> フォルダを作成します。<code>providers</code>フォルダ内に<code>TaskProvider.js</code>という名前でファイルを作成し、以下のコードを書きます。このコードの詳細については、コード中のコメントを参照してください。</p> <h4 id="./src/providers/TasksProvider.js"><a href="#.%2Fsrc%2Fproviders%2FTasksProvider.js">./src/providers/TasksProvider.js</a></h4> <pre><code class="jsx">import React, {useContext, useState, useEffect, useRef} from 'react'; import {openRealm, BSON} from '../realm'; const TasksContext = React.createContext(null); const TasksProvider = ({children}) => { const [tasks, setTasks] = useState([]); const realmRef = useRef(null); useEffect(() => { realmRef.current = openRealm(); const tasks = realmRef.current.objects('Task').sorted('createdAt', true); setTasks(tasks); // Task のデータが更新されたら setTasks する tasks.addListener(() => { const tasks = realmRef.current.objects('Task').sorted('createdAt', true); setTasks(tasks); }); return () => { // クリーンアップ if (realmRef.current) { realmRef.current.close(); } }; }, []); // タスクの新規作成 const createTask = (newTaskName) => { const projectRealm = realmRef.current; projectRealm.write(() => { projectRealm.create('Task', { _id: new BSON.ObjectId(), name: newTaskName || '新しいタスク', isDone: false, createdAt: new Date(), }); }); }; // タスクの isDone を更新する const setIsTaskDone = (task, isDone) => { const projectRealm = realmRef.current; projectRealm.write(() => { task.isDone = isDone; }); }; // タスクを削除する const deleteTask = (task) => { const projectRealm = realmRef.current; projectRealm.write(() => { projectRealm.delete(task); }); }; // useTasks フックで Task を操作できるようにする return ( <TasksContext.Provider value=<span>{</span><span>{</span> createTask, deleteTask, setIsTaskDone, tasks, <span>}</span><span>}</span>> {children} </TasksContext.Provider> ); }; // Task を操作するための React Hook const useTasks = () => { const task = useContext(TasksContext); if (task == null) { throw new Error('useTasks() called outside of a TasksProvider?'); } return task; }; export {TasksProvider, useTasks}; </code></pre> <p>以上で作成したTasksProviderをApp.jsに追加し、useTasksフックをコンポーネント内で使ってみます。ここでは、<code>src</code> フォルダ内に<code>components</code> フォルダを作成し、その中に<code>Main.js</code> というファイルでコンポーネントを定義します。</p> <h4 id="./App.js"><a href="#.%2FApp.js">./App.js</a></h4> <pre><code class="jsx">import React from 'react'; import {TasksProvider} from './src/providers/TasksProvider'; import {Main} from './src/components/Main'; import {StatusBar} from 'react-native'; const App = () => { return ( <TasksProvider> <StatusBar barStyle="light-content" /> <Main /> </TasksProvider> ); }; export default App; </code></pre> <h4 id="./src/components/Main.js"><a href="#.%2Fsrc%2Fcomponents%2FMain.js">./src/components/Main.js</a></h4> <pre><code class="jsx">import React, {useState, useCallback} from 'react'; import { SafeAreaView, FlatList, View, Text, TouchableOpacity, TextInput, KeyboardAvoidingView, StyleSheet, TouchableWithoutFeedback, Keyboard, } from 'react-native'; import {useTasks} from '../providers/TasksProvider'; // コンポーネント間の余白を作るための関数 const spacer = (size) => { return <View style=<span>{</span><span>{</span>height: size, width: size<span>}</span><span>}</span> />; }; export const Main = () => { const [inputText, setInputText] = useState(''); const {createTask, deleteTask, setIsTaskDone, tasks} = useTasks(); const onSubmitEditing = useCallback( (event) => { setInputText(event.nativeEvent.text); createTask(inputText); setInputText(''); }, [inputText, setInputText, createTask], ); return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}> <View style={styles.header}> <SafeAreaView /> <View style={styles.headerContent}> {spacer(15)} <Text accessibilityRole="header" style={styles.headerTitle}> My Tasks </Text> {spacer(15)} <TextInput placeholder="Add Task..." onSubmitEditing={onSubmitEditing} value={inputText} onChange={(e) => setInputText(e.nativeEvent.text)} style={styles.headerInput} /> {spacer(24)} </View> </View> <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}> {tasks.length > 0 ? ( <FlatList style={styles.tasksContainer} contentContainerStyle={styles.tasksContentContainer} data={tasks} keyExtractor={(item) => item._id.toHexString()} renderItem={({item}) => ( <View style={styles.taskItem}> <TouchableOpacity style={styles.chechboxContainer} onPress={() => setIsTaskDone(item, !item.isDone)}> <View style={[ styles.checkbox, { borderColor: item.isDone ? '#2563EB' : '#60A5FA', backgroundColor: item.isDone ? '#2563EB' : '#fff', }, ]}> {item.isDone && <Text style={styles.checkboxIcon}>✓</Text>} </View> </TouchableOpacity> <View style={styles.taskContent}> <Text style={[ styles.taskName, { textDecorationLine: item.isDone ? 'line-through' : 'none', color: item.isDone ? '#9CA3AF' : '#111827', }, ]}> {item.name} </Text> </View> <TouchableOpacity style={styles.deleteButton} onPress={() => deleteTask(item)}> <View> <Text style={styles.deleteButtonText}>Delete</Text> </View> </TouchableOpacity> </View> )} /> ) : ( <View style={styles.emptyContent}> <Text style={styles.emptyMessage}>No Tasks</Text> </View> )} </TouchableWithoutFeedback> </KeyboardAvoidingView> ); }; // スタイル const styles = StyleSheet.create({ container: {flex: 1}, header: { backgroundColor: '#2563EB', }, headerContent: {paddingHorizontal: 24}, headerTitle: {color: 'white', fontSize: 32, fontWeight: 'bold'}, headerInput: { borderRadius: 6, height: 40, paddingHorizontal: 12, marginHorizontal: -12, fontSize: 18, backgroundColor: '#fff', shadowColor: '#000', shadowOffset: { width: 0, height: 2.5, }, shadowOpacity: 0.3, shadowRadius: 2.4, elevation: 4, }, tasksContainer: {flex: 1}, tasksContentContainer: {paddingBottom: 20}, taskItem: { borderBottomWidth: 1, borderBottomColor: '#E5E7EB', flexDirection: 'row', alignItems: 'center', }, chechboxContainer: {padding: 20}, checkbox: { width: 25, height: 25, borderRadius: 12, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center', }, checkboxIcon: {color: 'white', fontWeight: 'bold', fontSize: 18}, taskContent: {flex: 1}, taskName: { fontSize: 20, fontWeight: '600', }, deleteButton: {padding: 20}, deleteButtonText: {color: '#EF4444'}, emptyContent: { justifyContent: 'center', alignItems: 'center', flex: 1, }, emptyMessage: { fontSize: 20, fontWeight: '500', color: '#9CA3AF', }, }); </code></pre> <h2 id="動作確認"><a href="#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">動作確認</a></h2> <p>ここまでできたら、一度動作確認してみます。今回は Simulator 上の iPhone で動作確認します。以下のコマンドで、シミュレータを立ち上げます。</p> <p>(以下のコマンドがエラーで失敗する場合、<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/66019068/event2-event-config-h-file-not-found">Flipperのバージョンを変更してみてください(Stack Overflow 英語版)</a>)</p> <pre><code class="bash">$ npx react-native run-ios </code></pre> <p>以下の動画のように動作すれば成功です。もしエラーが発生する場合、「DerivedDataの削除」「iosフォルダ内で<code>pod install</code> 再実行」などをお試しください(詳細はエラーメッセージでググってみてください)。</p> <p>ここまで書いてきたコードでは、タスクの編集ができません。タスク名を自由に編集できるようにしてみると良いかもしれません(本記事では省略します)。</p> <h2 id="スキーマ変更時の注意点(schemaVersionとマイグレーション)"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E5%A4%89%E6%9B%B4%E6%99%82%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9%28schemaVersion%E3%81%A8%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%29">スキーマ変更時の注意点(schemaVersionとマイグレーション)</a></h2> <p>アプリストアで配信中のアプリをアップデートする際、データベースのスキーマが変更になる場合、<code>schemaVersion</code> をインクリメントする必要があります。また、必要に応じて、旧スキーマのデータを新スキーマのデータへ移行する処理(マイグレーション)を書く必要があります。</p> <p>例として、Taskの<code>isDone</code> を変更してみます。ここまで、タスクの完了状況は<code>isDone</code>で管理してきました。<code>isDone</code> はタスクの完了・未完了しか表すことができないため、新しく<code>status</code> として、タスクの状態を以下の3つに分類できるようにしてみます。</p> <ul> <li>Open : タスク未着手</li> <li>InProgress : タスク進行中</li> <li>Complete : 完了</li> </ul> <h3 id="スキーマを書き換える"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%82%92%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B">スキーマを書き換える</a></h3> <p><code>./realm.js</code> に書いた Task のスキーマ定義を以下のように変更します。この作業はスキーマ定義を直接書き換えます。</p> <pre><code class="js">// 略 const taskSchema = { name: "Task", primaryKey: "_id", properties: { _id: "objectId", name: "string", description: "string?", // isDone: 'bool', status: "string", // isDone を削除し status を追加 createdAt: "date", subTasks: "SubTask[]", }, }; // 略 </code></pre> <h3 id="マイグレーション処理を書く"><a href="#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E6%9B%B8%E3%81%8F">マイグレーション処理を書く</a></h3> <p><code>./realm.js</code> ファイルを保存し、React Native開発環境を再度実行すると、コンソールに以下のようなエラーが表示されます。</p> <pre><code>Error: Migration is required due to the following errors: - Property 'Task.isDone' has been removed. - Property 'Task.status' has been added. </code></pre> <p>このエラーメッセージを日本語に訳すと以下のようになります。</p> <blockquote> <p>エラー: 以下のエラーのため、マイグレーションが必要です<br /> - プロパティ 'Task.isDone' が削除されました<br /> - プロパティ 'Task.status' が追加されました</p> </blockquote> <p>このようなエラーが表示された場合、<code>./realm.js</code> ファイルにある <code>openRealm</code> 関数内の <code>config</code> 定数を以下のように書き換えます。</p> <pre><code class="js">// 略 // Realmの初期化 export const openRealm = () => { const config = { schema: [taskSchema, subTaskSchema], // schemaVersion: 1, schemaVersion: 2, // ① schemaVersion を 1 → 2 へ変更 // ②マイグレーション処理を追加 migration: (oldRealm, newRealm) => { // 現在保存されているデータの schemaVersion が 2 未満の場合に実行 if (oldRealm.schemaVersion < 2) { const oldObjects = oldRealm.objects('Task'); const newObjects = newRealm.objects('Task'); // 全TaskデータのisDoneをstatusに変換 for (const objectIndex in oldObjects) { const oldObject = oldObjects[objectIndex]; const newObject = newObjects[objectIndex]; newObject.status = oldObject.isDone ? 'Complete' : 'Open'; } } }, }; return new Realm(config); }; // 略 </code></pre> <p>このファイルを保存し、再度アプリを実行すると、マイグレーションが実行され、データベースが更新されます。まだ isDone がコード上で使われているため、必要に応じて status を使うように修正を行ってください(本記事では省略します)。</p> <p>ここでのポイントは以下の2点です。</p> <h4 id="① schemaVersion を 1 → 2 へ変更"><a href="#%E2%91%A0+schemaVersion+%E3%82%92+1+%E2%86%92+2+%E3%81%B8%E5%A4%89%E6%9B%B4">① schemaVersion を 1 → 2 へ変更</a></h4> <p>Realm にデータベーススキーマが変更になったことを伝えるため、schemaVersion をインクリメントします。schemaVersion は、データベーススキーマに変更がある場合のみ変更します。一度 schemaVersion の値を増やした場合、この数値を減らさないでください。</p> <h4 id="② マイグレーション処理を追加"><a href="#%E2%91%A1+%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E8%BF%BD%E5%8A%A0">② マイグレーション処理を追加</a></h4> <p>ユーザーのスマホ内に保存されている古いスキーマのデータベース内のデータを、新しいスキーマのデータベースに変換する処理を書きます。多くの場合、oldRealmのデータをもとに、対応するnewRealm内のデータを全部書き換えるようなコードになると思います。もしくは、optional ではない新しいプロパティが追加された場合、そのプロパティの初期値を設定するコードになると思います。</p> <h2 id="RealmデータベースGUIで操作できる「Realm Studio」"><a href="#Realm%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9GUI%E3%81%A7%E6%93%8D%E4%BD%9C%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%8CRealm+Studio%E3%80%8D">RealmデータベースGUIで操作できる「Realm Studio」</a></h2> <p>最後に、Realm Studioを紹介します。Realm Studio はWindows、Linux、macOSに対応したRealmデータベースの管理アプリです。Realm Studio を使うことで、Realmデータベースの中身をGUI上で確認・操作することができます。</p> <p>Realm Studioは、Realm 公式サイト内にある<a target="_blank" rel="nofollow noopener" href="https://docs.realm.io/sync/realm-studio">Realm Studioのページ</a>からダウンロードできます。Realm Studio を初めて起動するとメールアドレスの登録画面が表示されるのでメールアドレスを登録します。その後、以下の画面が表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/8ede1c18125488061693d34088d212bf603cd9c3b4a55.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8ede1c18125488061693d34088d212bf603cd9c3b4a55.png?mw=700" alt="Realm Studioのスタート画面" /></a></p> <p>この画面が表示されたら [Open Realm file] をクリックし、Realm ファイルを開きます。Realm ファイルのパスは、以下のコードを<code>App.js</code>等に書いて実行することで確認できます(コンソールにファイルパスが表示されます)。</p> <pre><code class="js">import {openRealm} from './src/realm.js'; console.log(openRealm().path); </code></pre> <p>無事ファイルを開けると、以下のような画面になります。この画面から、データの確認・編集ができます。</p> <p><a href="https://crieit.now.sh/upload_images/93d1f6c129913c5f4eaba69e564ac993603cd9d1b9036.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/93d1f6c129913c5f4eaba69e564ac993603cd9d1b9036.png?mw=700" alt="Realm Studioの画面" /></a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>React Nativeアプリでオフラインデータを扱うライブラリとしてRealmを紹介してみました。クラウド上にデータを置かないアプリ開発の参考になれば幸いです。</p> <h2 id="参考文献"><a href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.mongodb.com/realm/react-native/">MongoDB Realm React Native SDK</a></li> </ul> SofPyon tag:crieit.net,2005:PublicArticle/15953 2020-06-14T15:24:10+09:00 2021-05-17T21:59:25+09:00 https://crieit.net/posts/introducing-portaldots 大学祭の参加団体向けウェブシステムをOSS化してみた <p><a href="https://crieit.now.sh/upload_images/658e61a355af5e8c729368deafa1ffd35ee268532719f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/658e61a355af5e8c729368deafa1ffd35ee268532719f.png?mw=700" alt="main_screenshot.png" /></a></p> <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>大学祭では、たくさんの参加団体(サークル・部活)が模擬店などの企画を出店し、盛り上げています。</p> <p>大学祭を成功させるには、実行委員会と参加団体の間の連携が欠かせません。連携するための方法として、実行委員会では、参加団体からの「各種申請」を受け付けています。例えば、大学祭当日に配布されるパンフレットに掲載する内容を参加団体から募集する必要があります。また、実行委員会が貸し出す備品の申請を受け付けることもあります。</p> <p>こうした申請受付業務は、多くの大学祭では紙による受付であったり、メールや Google フォームでの受付を行っているところが多いかと思います。</p> <p>紙やメールによる受付の場合、紙・メールに書かれた内容を1枚1枚Excelに入力していく手間がかかります。受付方法によっては対面での対応が必要となり、昨今の状況下では厳しいものがあります。<br /> (そもそも、今年の秋冬に開催される学園祭でも、予定通り開催できるかどうか怪しいところではありますが……)</p> <p>そのような中、私は<strong>大学祭の各種申請などを受け付けるウェブシステムを開発し、今年になってそれを OSS 化</strong>してみました。</p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a></li> <li>PortalDots 公式ウェブサイト(2020/11/30 公開) : <a target="_blank" rel="nofollow noopener" href="https://dots.soji.dev">https://dots.soji.dev</a></li> </ul> <h2 id="私は誰?"><a href="#%E7%A7%81%E3%81%AF%E8%AA%B0%EF%BC%9F">私は誰?</a></h2> <p>私は、東京理科大学の野田キャンパスに通う大学生です。大学名こそ「東京」とついていますが、「野田」は「千葉県」にあります。そんな野田キャンパスで開催される学園祭「野田地区理大祭」の実行委員をしていました。</p> <p>実行委員時代は、PortalDots の開発のほか、公式ウェブサイト・パンフレットのデザイン・実装なども行っていました。</p> <ul> <li>野田地区理大祭公式ウェブサイト : <a target="_blank" rel="nofollow noopener" href="https://nodaridaisai.com/">https://nodaridaisai.com/</a></li> </ul> <h2 id="参加団体向けウェブシステム「PortalDots」"><a href="#%E5%8F%82%E5%8A%A0%E5%9B%A3%E4%BD%93%E5%90%91%E3%81%91%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%80%8CPortalDots%E3%80%8D">参加団体向けウェブシステム「PortalDots」</a></h2> <p><a href="https://crieit.now.sh/upload_images/21e25532d98d0eaccd44964220ac8f765ee5e5b35aaeb.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/21e25532d98d0eaccd44964220ac8f765ee5e5b35aaeb.png?mw=700" height="40" alt="PortalDotsのロゴ"></a></p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a> <ul> <li>よろしければぜひ Star お願いします…!</li> </ul></li> </ul> <h3 id="開発環境の動かし方"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AE%E5%8B%95%E3%81%8B%E3%81%97%E6%96%B9">開発環境の動かし方</a></h3> <p>開発環境を動かすには Git、PHP(7.4以上)、Node.js、Yarn、Docker がセットアップ済みである必要があります。</p> <p><strong>2020/06/27 追記</strong> : コマンドの実行順序が間違っていたので修正しました。<br /> <strong>2021/05/17 追記</strong> : 開発環境の起動方法の変更を反映しました。</p> <pre><code class="bash">$ git clone [email protected]:portal-dots/PortalDots.git $ cd PortalDots/ # 必要な Node.js パッケージをインストール # ※ エラーが表示される場合は、Node.js を最新バージョンにアップグレードした上で、再度 yarn install を実行してください。 $ yarn install # 設定ファイルを作成 $ cp .env.example .env $ php artisan key:generate # 開発環境を起動する $ yarn docker # マイグレーション(データベースのセットアップ) $ yarn migrate # Docker コンテナ内で必要な PHP パッケージをインストール $ yarn docker-bash $ composer install $ exit # フロントエンド開発サーバーの起動 $ yarn hot # → ブラウザで http://localhost にアクセスすると、PortalDots の開発環境が起動する # → フロントエンド開発サーバーを終了するには Ctrl + C を押す # 開発環境を停止する $ yarn docker-stop </code></pre> <h3 id="トップページ"><a href="#%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8">トップページ</a></h3> <p>トップページでは、参加団体向け説明会の次回日程の表示機能や、各種お知らせの閲覧、配布資料のダウンロードなどができるようになっています。</p> <p>企画参加登録の受付期間中は、提出している参加登録の受理状況も確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/42a053619c899ae5db5c6cbc04f3d5eb5ee266d90c32e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/42a053619c899ae5db5c6cbc04f3d5eb5ee266d90c32e.png?mw=700" alt="screenshot_home.png" /></a></p> <h3 id="企画参加登録"><a href="#%E4%BC%81%E7%94%BB%E5%8F%82%E5%8A%A0%E7%99%BB%E9%8C%B2">企画参加登録</a></h3> <p>学園祭への企画エントリーもウェブから可能です。</p> <p><a href="https://crieit.now.sh/upload_images/942b1908f49e4c9db2e9466e90b229eb5ee267d059776.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/942b1908f49e4c9db2e9466e90b229eb5ee267d059776.png?mw=700" alt="screenshot_circle_register.png" /></a></p> <h3 id="申請フォーム"><a href="#%E7%94%B3%E8%AB%8B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0">申請フォーム</a></h3> <p>大学祭の参加団体は、このようなフォームからパンフレット掲載内容などの情報を委員会へ提出することができます。</p> <p>受付期間を設定することも可能です。</p> <p><a href="https://crieit.now.sh/upload_images/98cb9cc9bf6a8e93ec300a84080c92345ee2679b7ac94.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/98cb9cc9bf6a8e93ec300a84080c92345ee2679b7ac94.png?mw=700" alt="image.png" /></a></p> <h3 id="フォームエディター(Vue.js 製)"><a href="#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E3%83%BC%28Vue.js+%E8%A3%BD%29">フォームエディター(Vue.js 製)</a></h3> <p>「フォームエディター」は <strong>PortalDots の目玉機能(?)</strong> です。</p> <p>実行委員は、各種申請を受け付けるフォームを Google フォームのようなノリで作成することができるようになっています。</p> <p>「じゃあ、Google フォーム使えば良いのでは?」と思われるかもしれませんが、Google フォームではログイン・新規登録機能(※1)は利用できない上、回答内容の編集が容易でなかったり(※2)、<strong>1団体あたり1回答までに制限できなかったり</strong>(※3)するなど、大学祭の申請フォームとしては不便なところもあります。</p> <p>PortalDots の「フォームエディター」で作成できるフォームは、回答受付期間を設定できたり、1企画につき1回まで回答可能という設定ができたり、あとから回答を簡単に修正できたりします。</p> <p>※1 : Google フォームでも、一応 Google アカウントでのログインを必須にすることはできます</p> <p>※2 : Google フォームでも、回答者に回答の編集を許可することはできます。ただ、編集用の URL を紛失してしまうと編集できなくなってしまいます</p> <p>※3 : Google フォームでは、<strong>1ユーザーあたり</strong>の回答数を制限できます。<strong>1団体あたり</strong>のような制限をかけるのは難しいと思われます</p> <p><a href="https://crieit.now.sh/upload_images/fd82398a4a47ec43ce8edc68a27147cd5ee2676b1a9f0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fd82398a4a47ec43ce8edc68a27147cd5ee2676b1a9f0.png?mw=700" alt="screenshot_form_editor.png" /></a></p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">参加企画に提出してもらう各種申請もウェブフォームで受け付けられます。動画のように設問ぐりぐりできます。 <a target="_blank" rel="nofollow noopener" href="https://t.co/5rVFRVnUPd">pic.twitter.com/5rVFRVnUPd</a></p>— Soji — PortalDots (@sofpyon) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sofpyon/status/1271122648928403456?ref_src=twsrc%5Etfw">June 11, 2020</a></blockquote> <h2 id="CodeIgniter から Laravel への移行途中です → 完了しました!"><a href="#CodeIgniter+%E3%81%8B%E3%82%89+Laravel+%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C%E9%80%94%E4%B8%AD%E3%81%A7%E3%81%99+%E2%86%92+%E5%AE%8C%E4%BA%86%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">CodeIgniter から Laravel への移行途中です → 完了しました!</a></h2> <p><strong>2021/05/17追記 : 先日、CodeIgniter のコードを全て削除し、Laravel へ完全移行しました!</strong></p> <blockquote class="twitter-tweet" data-lang="ja" data-dnt="true"><p lang="ja" dir="ltr">ついに <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/PortalDots?src=hash&ref_src=twsrc%5Etfw">#PortalDots</a> のコードが、悲願の 100% Laravel 化達成しました…!CodeIgniter 消します! <a target="_blank" rel="nofollow noopener" href="https://t.co/vt1RKYDfcL">pic.twitter.com/vt1RKYDfcL</a></p>— Soji (@sofpyon) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sofpyon/status/1388511050224472066?ref_src=twsrc%5Etfw">2021年5月1日</a></blockquote> <hr /> <p>(以下、追記前)</p> <p>現在、「CodeIgniter」と「Laravel」という、2つのウェブフレームワークを混在して使用しています。</p> <p>元々 PortalDots は、私がウェブフレームワーク初心者のころ(大学1年の夏)に開発を始めた物でした。それ以前はフレームワークという物自体を使ったことがなく、プレーンな PHP コードでしか書いたことがなかったのですが、「CodeIgniter というフレームワークは簡単」だという話を聞き、試しに PortalDots の開発で使ってみたのでした。</p> <p>しかし、CodeIgniter の機能は貧弱である点や、なるべくメインストリームにあるフレームワークを使ったほうが今後のメンテナンスがしやすいだろうということで、Laravel への移行をはじめました。</p> <p>2020年6月現在、CodeIgniter が使われているのは「スタッフモード」(実行委員用のページ)のみとなっており、それ以外のページは Laravel に移行済みとなっています。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>OSS 化したので PortalDots はどなたでも自由にお使いいただけるようになりました。</p> <p>学園祭実行委員会に所属しており、かつプログラミング経験のある方がいましたら、ぜひ使っていただきたいです。また、学園祭関係者でなくても、お試しとして実物を触っていただけると嬉しいです!</p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a></li> <li>PortalDots 公式ウェブサイト(2020/11/30 公開) : <a target="_blank" rel="nofollow noopener" href="https://dots.soji.dev">https://dots.soji.dev</a></li> </ul> SofPyon