モバイルアプリのデータ保存入門:SQLite、Room、Core Dataの基本を徹底解説!

モバイルアプリ開発

はじめに:なぜデータ保存が重要なのか? 🤔

スマートフォンアプリを開発する上で、「データの保存」は避けて通れない重要なテーマです。ユーザーの設定情報、アプリの利用状況、オフラインで利用するためのコンテンツなど、様々なデータをアプリ内に保持する必要があります。

適切なデータ保存戦略は、以下のようなメリットをもたらします。

  • オフライン対応: ネットワーク接続がない状況でも、保存されたデータを使ってアプリの基本機能を提供できます。
  • パフォーマンス向上: 頻繁にアクセスするデータをローカルに保存しておくことで、ネットワーク通信の待ち時間を削減し、アプリの応答性を高められます。
  • ユーザー体験の向上: ユーザーの入力履歴や設定を記憶しておくことで、よりパーソナライズされた快適な操作感を提供できます。
  • データ同期の基盤: ローカルにデータを保存し、ネットワーク接続時にサーバーと同期する、といったアーキテクチャの基礎となります。

モバイルアプリでローカルにデータを保存する方法はいくつかありますが、特にリレーショナルデータを扱う場合、データベースを利用するのが一般的です。この記事では、モバイルプラットフォームで広く使われている以下の3つの技術について、基本的な概念から使い方までを丁寧に解説していきます。

  1. SQLite: 軽量なファイルベースのリレーショナルデータベース管理システム (RDBMS)。
  2. Room Persistence Library: Android公式のSQLite抽象化ライブラリ (ORM)。
  3. Core Data: Appleプラットフォーム (iOS, macOSなど) 向けのデータ永続化フレームワーク。

これらの技術は、それぞれ特徴や得意なことが異なります。この記事を通して、それぞれの基本を理解し、あなたのアプリ開発に最適なデータ保存方法を選択するための一助となれば幸いです。✨

SQLite: 軽量データベースのスタンダード 💾

SQLiteは、モバイルアプリ開発におけるローカルデータストレージのデファクトスタンダードとも言える、非常に人気のあるリレーショナルデータベース管理システム (RDBMS) です。

SQLiteの基本概念

リレーショナルデータベースとは、データをテーブル(表)の形式で管理し、テーブル間の関連(リレーションシップ)を定義できるデータベースのことです。Excelのシートのようなものを想像すると分かりやすいかもしれません。各テーブルは行(レコード)列(カラム)で構成され、各列には特定のデータ型(数値、文字列、日付など)が定義されます。

SQLiteの最大の特徴は、その軽量さ手軽さにあります。

  • ファイルベース: データベース全体が単一のファイルとして保存されます。サーバープロセスを必要とせず、アプリにライブラリとして組み込んで直接ファイルを操作します。
  • サーバーレス: MySQLやPostgreSQLのようなクライアント/サーバー型のデータベースとは異なり、独立したサーバープロセスを必要としません。アプリのプロセス内で直接動作します。
  • 設定不要: 複雑な設定や管理作業はほとんど必要ありません。
  • トランザクション対応: ACID特性 (原子性, 一貫性, 独立性, 永続性) をサポートしており、複数のデータ操作を一つのまとまりとして扱い、データの整合性を保証します。
  • 標準SQL準拠: 多くの標準SQLクエリをサポートしています (一部方言や制限はあります)。
  • クロスプラットフォーム: C言語で書かれており、Android, iOS, Windows, macOS, Linuxなど、非常に多くのプラットフォームで利用可能です。

メリットとデメリット

メリット 👍

  • 軽量で高速
  • 導入が容易(多くのプラットフォームで標準的に利用可能)
  • 単一ファイルで管理がしやすい
  • サーバー不要でアプリに組み込みやすい
  • 安定性と信頼性が高い (長年の実績)
  • クロスプラットフォーム開発で共通のDBを使える

デメリット 👎

  • 高負荷な同時書き込みには向かない (ファイルロックのため)
  • 複雑なネットワーク機能やユーザー管理機能はない
  • SQL文を直接書く必要があり、記述量が多くなりがち
  • SQL文のタイプミスなどは実行時まで気づきにくい
  • オブジェクト指向言語とのインピーダンスミスマッチ(データをオブジェクトにマッピングする手間)

基本的な使い方 (SQLの概念)

SQLiteを操作するには、基本的にSQL (Structured Query Language) と呼ばれる言語を使用します。以下は、代表的なSQL文の例です。

  • テーブル作成 (CREATE TABLE): データを格納するためのテーブル構造を定義します。
    -- ユーザー情報を格納するテーブルを作成
    CREATE TABLE users (
      id INTEGER PRIMARY KEY AUTOINCREMENT, -- ID (自動採番される主キー)
      name TEXT NOT NULL,                   -- 名前 (必須)
      email TEXT UNIQUE,                    -- メールアドレス (一意)
      age INTEGER
    );
  • データ挿入 (INSERT): テーブルに新しいデータを追加します。
    -- 新しいユーザーを追加
    INSERT INTO users (name, email, age) VALUES ('山田 太郎', 'taro@example.com', 30);
    INSERT INTO users (name, email) VALUES ('佐藤 花子', 'hanako@example.com');
  • データ取得 (SELECT): テーブルからデータを検索・取得します。
    -- 全てのユーザーを取得
    SELECT * FROM users;
    
    -- 特定の条件 (30歳以上) のユーザーの名前とメールアドレスを取得
    SELECT name, email FROM users WHERE age >= 30;
    
    -- IDが1のユーザーを取得
    SELECT * FROM users WHERE id = 1;
  • データ更新 (UPDATE): 既存のデータを変更します。
    -- IDが1のユーザーの年齢を更新
    UPDATE users SET age = 31 WHERE id = 1;
  • データ削除 (DELETE): テーブルからデータを削除します。
    -- IDが2のユーザーを削除
    DELETE FROM users WHERE id = 2;
    
    -- 全てのユーザーを削除 (注意!)
    DELETE FROM users;

実際のアプリ開発では、各プラットフォームが提供するAPI(Androidの `android.database.sqlite` パッケージやiOSの `FMDB` などのサードパーティライブラリ)を使って、これらのSQL文を実行し、結果を受け取って処理します。

Android/iOSでの利用

Android では、標準でSQLiteがサポートされており、`android.database.sqlite` パッケージを通じて直接操作できます。ただし、SQL文の組み立てやカーソルからのデータ読み取りなど、定型的なコードが多くなりがちです。そのため、後述する Room を使うのが現在の主流です。

iOS では、直接SQLiteを操作するためのC言語APIがありますが、より扱いやすくするために `FMDB` のようなサードパーティのラッパーライブラリを使うことが一般的でした。しかし、Appleは公式のフレームワークとして Core Data を提供しており、多くの場合はこちらを利用します (Core Dataの内部ストレージとしてSQLiteが使われることもあります)。

SQLite自体は非常に強力で柔軟なデータベースですが、モバイルアプリ開発においては、より高レベルな抽象化を提供するRoomやCore Dataを使うことで、開発効率や安全性を高めることができます。

🔗 参考: SQLite 公式サイト

Room Persistence Library (Android) 🏠

Roomは、Googleが提供するAndroid Jetpackの一部であり、SQLiteデータベースの上に構築された抽象化レイヤーです。より堅牢で効率的なデータベースアクセスを実現するためのライブラリであり、ORM (Object-Relational Mapping) ライブラリの一種と考えることができます。

なぜRoomを使うのか?

生のSQLite APIを使う場合と比較して、Roomには以下のようなメリットがあります。

  • コンパイル時のSQL検証: SQLクエリの構文エラーやテーブル/カラム名の typo などを、アプリの実行時ではなくコンパイル時に検出できます。これにより、実行時エラーのリスクを大幅に削減できます。
  • ボイラープレートコードの削減: SQL文の組み立て、`Cursor` からオブジェクトへの変換といった定型的なコードの多くをRoomが自動生成してくれます。開発者はより本質的なロジックに集中できます。
  • オブジェクトマッピング: JavaやKotlinのオブジェクト(エンティティ)とデータベースのテーブル構造を簡単にマッピングできます。
  • LiveData / Flow との連携: データベースの変更を監視し、UIを自動的に更新するための仕組み (LiveDataやKotlin Flow) とスムーズに連携できます。
  • マイグレーションの簡略化: アプリのバージョンアップに伴うデータベーススキーマの変更(マイグレーション)をサポートする仕組みが提供されています。

基本的に、AndroidでSQLiteを利用する場合は、生のAPIを直接使うのではなく、Roomの利用が強く推奨されています。

主要コンポーネント

Roomは主に以下の3つのコンポーネントから構成されます。

  1. @Entity (エンティティ)

    データベース内のテーブルを表すクラスです。各インスタンスがテーブルの1行に対応します。@Entity アノテーションを付与し、クラスのフィールドがテーブルのカラムに対応します。@PrimaryKey で主キーを、@ColumnInfo でカラム名を指定できます。

  2. @Dao (Data Access Object / データアクセスオブジェクト)

    データベースへのアクセス(クエリ)を定義するインターフェースまたは抽象クラスです。@Dao アノテーションを付与します。具体的なSQLクエリは @Insert, @Update, @Delete, @Query といったアノテーションを使ってメソッドに紐付けます。Roomがコンパイル時にこれらのメソッドの実装を自動生成します。

  3. @Database (データベース)

    データベース全体を表す抽象クラスです。@Database アノテーションを付与し、以下の情報を含みます。

    • データベースに含まれるエンティティのリスト (entities プロパティ)
    • データベースのバージョン (version プロパティ)
    • DAOを取得するための抽象メソッド

    このクラスのインスタンスを通じて、アプリはDAOを取得し、データベース操作を行います。通常、シングルトンとして実装されます。

基本的な使い方 (Kotlinの例)

簡単なユーザー管理を例に、Roomの基本的な使い方を見てみましょう。

1. ライブラリの依存関係を追加 (build.gradle)

// app/build.gradle.kts または app/build.gradle

dependencies {
    val roomVersion = "2.6.1" // 最新バージョンを確認してください

    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")

    // Kotlin向け拡張機能 (任意ですが推奨)
    implementation("androidx.room:room-ktx:$roomVersion")

    // Test helpers (任意)
    testImplementation("androidx.room:room-testing:$roomVersion")

    // KSP (Kotlin Symbol Processing) を使う場合 (推奨)
    // annotationProcessor を ksp に置き換える
    // ksp("androidx.room:room-compiler:$roomVersion")
}

// KSPを使う場合は、プロジェクトレベルの build.gradle.kts または build.gradle にプラグインを追加
// plugins {
//     id("com.google.devtools.ksp") version "..." apply false
// }
// appレベルの build.gradle.kts または build.gradle にプラグインを適用
// plugins {
//     id("com.google.devtools.ksp")
// }

※ 上記はGradle Kotlin DSLの例です。Groovyの場合は書き方が異なります。

※ 最新のバージョンや設定方法は公式ドキュメントで確認してください。

2. Entity (エンティティ) の定義

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users") // テーブル名を指定 (省略するとクラス名)
data class User(
    @PrimaryKey(autoGenerate = true) // 主キー、自動生成
    val uid: Int = 0, // デフォルト値を与えると autoGenerate が効く

    @ColumnInfo(name = "first_name") // カラム名を指定 (省略するとフィールド名)
    val firstName: String?,

    @ColumnInfo(name = "last_name")
    val lastName: String?,

    val age: Int? // Nullableな型もOK
)

3. Dao (データアクセスオブジェクト) の定義

import androidx.room.*
import kotlinx.coroutines.flow.Flow // Flowを使う例

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAll(): List<User> // 同期的に全件取得

    @Query("SELECT * FROM users")
    fun getAllFlow(): Flow<List<User>> // Flowで変更を監視

    @Query("SELECT * FROM users WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM users WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User? // Null許容

    @Insert(onConflict = OnConflictStrategy.IGNORE) // 競合時は無視する
    suspend fun insert(user: User) // suspend関数で非同期実行 (Coroutines)

    @Insert
    suspend fun insertAll(vararg users: User) // 可変長引数で複数挿入

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("DELETE FROM users WHERE uid = :userId")
    suspend fun deleteById(userId: Int)
}

※ データベース操作はメインスレッドで行うべきではないため、suspend 関数や FlowLiveData を利用して非同期に実行するのが一般的です。

4. Database (データベース) クラスの定義

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [User::class], version = 1, exportSchema = false) // exportSchemaは通常falseでOK
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao // Daoを取得する抽象メソッド

    companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time.
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database" // データベースファイル名
                )
                // .fallbackToDestructiveMigration() // マイグレーション戦略 (これは単純な例)
                .build()
                INSTANCE = instance
                // return instance
                instance
            }
        }
    }
}

5. データベースインスタンスの取得と利用

import kotlinx.coroutines.launch // Coroutinesを使う例
import kotlinx.coroutines.runBlocking // 簡単な例のため runBlocking を使用

// ActivityやViewModelなどで

// データベースインスタンスを取得 (通常はDIライブラリなどを使う)
val db = AppDatabase.getDatabase(applicationContext)
val userDao = db.userDao()

// --- 例: Coroutineスコープ内で非同期に実行 ---

// データの挿入
// viewModelScope.launch { // ViewModelの場合
runBlocking { // 簡単な実行例 (実際のアプリでは適切なCoroutineScopeを使う)
    val newUser = User(firstName = "Taro", lastName = "Yamada", age = 30)
    userDao.insert(newUser)
    println("User inserted!")
}

// データの取得 (Flowで監視)
// viewModelScope.launch {
//     userDao.getAllFlow().collect { users ->
//         // usersリストが変更されるたびにここが呼ばれる
//         println("Current users: $users")
//     }
// }

// データの取得 (同期的に全件)
runBlocking {
    val allUsers = userDao.getAll()
    println("All users (sync): $allUsers")
}

// データの更新 (例: 取得したユーザーの年齢を更新)
runBlocking {
    val userToUpdate = userDao.findByName("Taro", "Yamada")
    if (userToUpdate != null) {
        val updatedUser = userToUpdate.copy(age = 31)
        userDao.update(updatedUser)
        println("User updated!")
    }
}

// データの削除 (例: IDで削除)
runBlocking {
     val userToDelete = userDao.findByName("Taro", "Yamada")
     if(userToDelete != null) {
         userDao.deleteById(userToDelete.uid)
         println("User deleted!")
     }
}

RoomはSQLiteの複雑さを隠蔽し、より安全でモダンな方法でデータベースを扱うことを可能にします。Android開発においては、データ永続化の第一選択肢と言えるでしょう。

Core Data (iOS / macOS) 🍎

Core Dataは、Appleが提供するiOS, macOS, watchOS, tvOS向けのデータ永続化フレームワークです。単なるデータベースライブラリではなく、オブジェクトグラフ管理と永続化のための包括的な機能を提供します。

⚠️ Core DataはORMではありません

よく誤解されがちですが、Core Dataは厳密にはORM (Object-Relational Mapping) ではありません。もちろん、SQLiteストアを使用すれば結果的にオブジェクトとリレーショナルデータベースのマッピングを行いますが、Core Dataの主目的はオブジェクトグラフ(相互に関連するオブジェクトの集まり)のライフサイクル管理、永続化、キャッシュ、変更追跡、アンドゥ/リドゥ機能などを提供することにあります。永続化ストアとしてSQLite以外(バイナリ形式、XML形式、インメモリ)も選択可能です。

なぜCore Dataを使うのか?

Core Dataを利用する主なメリットは以下の通りです。

  • オブジェクトグラフ管理: オブジェクト間のリレーションシップ(対一、対多)を簡単に定義・管理できます。関連オブジェクトの取得や操作が直感的に行えます。
  • 永続化の抽象化: データの保存先(SQLite, XML, バイナリ, インメモリ)を意識せずに、オブジェクトとしてデータを扱えます。後から保存形式を変更することも比較的容易です。
  • 変更追跡とアンドゥ/リドゥ: 管理対象オブジェクトへの変更が自動的に追跡され、簡単に変更を保存したり、元に戻したり(アンドゥ/リドゥ)できます。
  • メモリ効率: フォールティング(Faulting)と呼ばれる遅延読み込みメカニズムにより、必要になるまでオブジェクトのデータをメモリに読み込まず、メモリ使用量を抑えることができます。
  • データ検証: モデル定義時に属性のバリデーションルール(必須、最小/最大値、正規表現など)を設定できます。
  • マイグレーションサポート: スキーマ変更時のデータ移行(マイグレーション)を支援する機能が組み込まれています。
  • パフォーマンス: 大量データの扱いや複雑なクエリに対して最適化されています (適切に使用すれば)。
  • iCloud連携: Core DataのデータをiCloud経由で複数デバイス間で同期させる機能も提供されています。

主要コンポーネント (Core Data Stack)

Core Dataを利用する際には、一般的に以下のコンポーネントからなる「Core Dataスタック」を構築します。

  1. Managed Object Model (管理対象オブジェクトモデル / NSManagedObjectModel)

    データベースのスキーマ定義に相当します。通常、Xcodeのデータモデルエディタ (.xcdatamodeld ファイル) を使ってGUIで作成します。エンティティ (Entity)、属性 (Attribute)、リレーションシップ (Relationship) などを定義します。

    • Entity: アプリ内のオブジェクトの型(クラス)を表します。SQLiteストアを使う場合はテーブルに相当します。
    • Attribute: エンティティが持つプロパティ(データ)です。String, Integer, Date, Dataなどの型を指定します。SQLiteストアを使う場合はカラムに相当します。
    • Relationship: エンティティ間の関連を定義します。対一 (To-One) または対多 (To-Many) の関係、逆方向のリレーションシップ、削除ルールなどを設定できます。
  2. Persistent Store Coordinator (永続ストアコーディネータ / NSPersistentStoreCoordinator)

    Managed Object Modelと実際のデータストア(永続ストア)の間の調整役です。モデル情報を元に、どのストア(SQLiteファイル、XMLファイルなど)にデータを読み書きするかを管理します。通常、アプリには一つのコーディネータが存在します。

  3. Managed Object Context (管理対象オブジェクトコンテキスト / NSManagedObjectContext)

    アプリケーションがCore Dataとやり取りするための主要なインターフェースです。オブジェクトの作業場スクラッチパッドのようなものと考えられます。開発者は主にこのContextを通じて、データの取得 (Fetch)、作成 (Create)、変更 (Modify)、削除 (Delete) を行います。Contextは変更を追跡しており、save() メソッドが呼ばれると、変更内容がPersistent Store Coordinatorを通じて永続ストアに書き込まれます。

    Contextは複数作成でき、スレッドセーフティのために、通常はメインスレッド用とバックグラウンド処理用に分けて使われます。

  4. Managed Object (管理対象オブジェクト / NSManagedObject)

    データベース内のレコード(行)に相当するオブジェクトです。Entity定義に基づいて生成され、Contextによってライフサイクルが管理されます。開発者はこのオブジェクトのプロパティを読み書きすることでデータを操作します。

  5. Persistent Container (永続コンテナ / NSPersistentContainer) – iOS 10+

    上記のスタック(Model, Coordinator, Context)のセットアップを簡略化するために導入されたクラスです。多くの場合、このクラスを使うことでCore Dataスタックの初期化コードを大幅に削減できます。

基本的な使い方 (Swiftの例)

簡単なユーザー管理を例に、Core Dataの基本的な使い方を見てみましょう。(ここでは NSPersistentContainer を使用します)

1. データモデルの作成 (.xcdatamodeld)

Xcodeで新規プロジェクト作成時に「Use Core Data」にチェックを入れるか、後から「File」>「New」>「File…」>「Data Model」を選択して .xcdatamodeld ファイルを作成します。

エディタ上で「Add Entity」をクリックし、エンティティ名を (例: `User`) とします。Attributesインスペクタで属性を追加します (例: `userId` (UUID), `firstName` (String), `lastName` (String), `age` (Integer 16))。

コード生成の設定 (Codegen) を “Class Definition” にすると、Xcodeが自動的に `NSManagedObject` のサブクラス (例: `User+CoreDataClass.swift`, `User+CoreDataProperties.swift`) を生成してくれます。

2. NSPersistentContainer のセットアップ

通常、AppDelegateや専用の管理クラスで NSPersistentContainer を初期化します。

import CoreData
import UIKit // AppDelegateを使う場合

// AppDelegate.swift など

lazy var persistentContainer: NSPersistentContainer = {
    // "YourDataModelName" は .xcdatamodeld ファイルの名前に合わせる
    let container = NSPersistentContainer(name: "YourDataModelName")

    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            // エラー処理 (クラッシュさせるか、適切なフォールバック処理)
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
        // print("Core Data Store Loaded: \(storeDescription)")
        container.viewContext.automaticallyMergesChangesFromParent = true // オプション: バックグラウンドContextからの変更を自動マージ
    })
    return container
}()

// 保存処理用のヘルパーメソッド
func saveContext () {
    let context = persistentContainer.viewContext // メインスレッド用Context
    if context.hasChanges {
        do {
            try context.save()
            // print("Context saved successfully.")
        } catch {
            // エラー処理
            let nserror = error as NSError
            // fatalError("Unresolved error \(nserror), \(nserror.userInfo)") // 開発中はクラッシュ推奨
            print("Error saving context: \(nserror), \(nserror.userInfo)")
        }
    }
}

// メインスレッド用Contextへの便利なアクセス
var viewContext: NSManagedObjectContext {
    return persistentContainer.viewContext
}

// バックグラウンド処理用の新しいContextを生成するメソッド (例)
func newBackgroundContext() -> NSManagedObjectContext {
    return persistentContainer.newBackgroundContext()
}

※ 上記は基本的なセットアップ例です。エラーハンドリングやマイグレーション設定は、実際のアプリに合わせて調整が必要です。

3. データの作成 (Create)

// 通常はメインスレッドで実行 (UIイベントなど)
let context = viewContext // AppDelegateなどから取得

let newUser = User(context: context) // 新しいManaged Objectを作成
newUser.userId = UUID()
newUser.firstName = "Hanako"
newUser.lastName = "Sato"
newUser.age = 25

// 変更を保存
saveContext() // AppDelegateのヘルパーメソッドを呼ぶ

4. データの取得 (Fetch)

let context = viewContext

// Fetch Request を作成
let fetchRequest: NSFetchRequest<User> = User.fetchRequest()

// (オプション) 検索条件 (Predicate) を設定
// 例: 姓が "Sato" のユーザーを検索
// fetchRequest.predicate = NSPredicate(format: "lastName == %@", "Sato")

// (オプション) ソート順 (Sort Descriptors) を設定
// 例: 年齢の昇順でソート
// let sortDescriptor = NSSortDescriptor(key: "age", ascending: true)
// fetchRequest.sortDescriptors = [sortDescriptor]

do {
    let users = try context.fetch(fetchRequest)
    // users は User オブジェクトの配列
    for user in users {
        print("Fetched User: \(user.firstName ?? "") \(user.lastName ?? ""), Age: \(user.age)")
    }
} catch {
    print("Failed to fetch users: \(error)")
}

@FetchRequest プロパティラッパーを使えば、SwiftUIビュー内でより宣言的にデータを取得・表示できます。

5. データの更新 (Update)

let context = viewContext
let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
// 更新したいユーザーを検索 (例: Hanako Sato)
fetchRequest.predicate = NSPredicate(format: "firstName == %@ AND lastName == %@", "Hanako", "Sato")
fetchRequest.fetchLimit = 1 // 1件だけ取得

do {
    let results = try context.fetch(fetchRequest)
    if let userToUpdate = results.first {
        // プロパティを変更
        userToUpdate.age = 26
        // 変更を保存
        saveContext()
        print("User updated.")
    } else {
        print("User not found.")
    }
} catch {
    print("Failed to fetch or update user: \(error)")
}

6. データの削除 (Delete)

let context = viewContext
let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
// 削除したいユーザーを検索 (例: Hanako Sato)
fetchRequest.predicate = NSPredicate(format: "firstName == %@ AND lastName == %@", "Hanako", "Sato")
fetchRequest.fetchLimit = 1

do {
    let results = try context.fetch(fetchRequest)
    if let userToDelete = results.first {
        // Contextからオブジェクトを削除
        context.delete(userToDelete)
        // 変更を保存
        saveContext()
        print("User deleted.")
    } else {
        print("User not found.")
    }
} catch {
    print("Failed to fetch or delete user: \(error)")
}

Core Dataは学習コストが比較的高いと言われますが、オブジェクト指向的なアプローチでデータを扱え、Appleプラットフォームとの親和性も高いため、iOS/macOSアプリ開発において強力な選択肢となります。

SQLite, Room, Core Data の比較と選択基準 🤔🔄

ここまで、SQLite、Room、Core Dataの基本的な特徴と使い方を見てきました。では、実際にアプリ開発でどれを選べば良いのでしょうか?それぞれの技術をいくつかの観点から比較してみましょう。

比較表

観点 SQLite Room (Android) Core Data (iOS/macOS)
プラットフォーム クロスプラットフォーム (Android, iOS, Web, Desktopなど多数) Android 専用 Appleプラットフォーム専用 (iOS, macOS, watchOS, tvOS)
抽象化レベル 低 (SQL直書き) 高 (ORMライク) 高 (オブジェクトグラフ管理)
主な操作方法 SQL文 DAOインターフェース経由のメソッド呼び出し (内部でSQL生成) Managed Object Context経由のオブジェクト操作 (Fetch Requestなど)
SQL知識 必須 基本的な知識は役立つ (複雑なクエリはSQLを書くことも) 必須ではない (内部で利用されるが隠蔽されている)
コンパイル時チェック なし (実行時エラー) あり (SQL構文、スキーマ参照) 一部あり (モデル定義)、Fetch Requestの型安全性は限定的
ボイラープレートコード 多い (SQL文、カーソル処理、オブジェクト変換) 少ない (Roomが自動生成) 比較的少ない (スタック設定は必要だが、データ操作はオブジェクト指向)
オブジェクト指向との親和性 低い (インピーダンスミスマッチ) 高い (オブジェクトとテーブルをマッピング) 非常に高い (オブジェクトグラフとして管理)
学習コスト 中 (SQLの知識が必要) 低〜中 (基本的な使い方は容易) 高 (独自の概念やコンポーネントが多い)
主な用途 クロスプラットフォーム開発、低レベル制御が必要な場合、シンプルなデータ構造 Androidネイティブアプリでの標準的なデータ永続化 Appleプラットフォームでのリッチなデータ永続化、オブジェクトグラフ管理、オフライン機能
LiveData/Flow/Combine連携 自前実装が必要 容易 (標準でサポート) 可能 (NSFetchedResultsController や Combine Publisher を利用)
エコシステム 多数の言語・プラットフォームに対応 Android Jetpackの一部として強力にサポート Apple Developerエコシステムに統合 (Xcode, SwiftUI, Combine, iCloud)

どの技術を選ぶべきか?

絶対的な正解はなく、プロジェクトの要件や制約によって最適な選択は異なります。

SQLite を選ぶ場合

  • 🚀 クロスプラットフォーム開発: Kotlin Multiplatform Mobile (KMM), Flutter, React Native などで、共通のデータベースロジックを使いたい場合 (通常は各フレームワーク用のSQLiteラッパーライブラリと併用)。
  • ⚙️ 低レベルな制御: パフォーマンスチューニングのためにSQLを細かく制御したい、特殊なSQLiteの機能を使いたい場合。
  • 🌱 非常にシンプルなデータ構造: 保存するデータがごく僅かで、ORMのオーバーヘッドが気になる場合 (ただし、多くの場合Room/Core Dataの方が開発効率は高い)。
  • 📚 既存のSQLite資産: 既に存在するSQLiteデータベースファイルを活用したい場合。

Room を選ぶ場合

  • 🤖 Androidネイティブアプリ開発: ほぼ全ての場合において、Androidでのローカルデータベース利用の第一候補。
  • 安全性と生産性: コンパイル時チェックによる安全性向上と、ボイラープレート削減による生産性向上が期待できる。
  • 🔄 リアクティブなUI: LiveDataやFlowを使って、データベースの変更に追従するUIを簡単に実装したい場合。
  • 📈 モダンなAndroid開発: Jetpack Compose, Coroutines, Hilt/Daggerなど、他のモダンなAndroid開発技術との連携がスムーズ。

選択にあたっては、開発するプラットフォーム、データの複雑さ、チームのスキルセット、開発速度、将来的な拡張性などを総合的に考慮することが重要です。

まとめ 🏁

この記事では、モバイルアプリ開発における主要なローカルデータ保存技術であるSQLite、Room、Core Dataについて、それぞれの基本的な概念、特徴、使い方、そして比較を行いました。

  • SQLite は、軽量でクロスプラットフォームなファイルベースのRDBMSであり、多くの場面での基礎となりますが、直接使う場合はSQLの知識と定型コードの記述が必要です。
  • Room は、Android開発におけるSQLiteの推奨ラッパーであり、コンパイル時チェックやボイラープレート削減、LiveData/Flow連携などにより、安全かつ効率的なデータベースアクセスを提供します。
  • Core Data は、Appleプラットフォーム向けの強力なオブジェクトグラフ管理・永続化フレームワークであり、複雑なデータモデルやリレーションシップ、変更追跡などを扱うのに適しています。

どの技術を選択するかは、アプリの要件、対象プラットフォーム、開発チームの経験などによって異なります。それぞれのメリット・デメリットを理解し、プロジェクトに最適なものを選びましょう。場合によっては、これらの技術を組み合わせたり、他のデータ保存方法(Key-Valueストア、ファイルI/Oなど)を検討することも有効です。

データ永続化は奥が深い分野ですが、この記事がその第一歩を踏み出す助けとなれば幸いです。ぜひ公式ドキュメントなども参照し、さらに理解を深めてみてください。Happy Coding! 💻📱

コメント

タイトルとURLをコピーしました