AndroidでSQLiteを扱うとき、Roomは非常に有力な選択肢です。
一方で、
- SQLの構造は自分で意識して組み立てたい
- 動的な検索条件をKotlinコードで構築したい
- JOIN、サブクエリ、HAVINGなどを柔軟に扱いたい
- SQL文字列の連結やバインド値の管理は共通化したい
- Entityの定義やCursorからの変換処理はできるだけ省力化したい
という開発スタイルもあると思います。
そこで、**SQLの自由度を残しながら、Kotlinのdata classとKSPでSQLite開発を支援するAndroid向けORM「AndrORM」**を開発しています。
この記事では、AndrORMがどのようなライブラリなのか、Roomと何が違うのか、どのように使うのかを紹介します。
AndrORMは現在アルファ版です。
この記事は0.1.5-alphaを前提としています。
今後、APIや仕様が変更される可能性があります。
AndrORMとは
AndrORMは、Android/Kotlin向けのSQLite ORMです。
SQLを隠蔽することを目的としたORMではありません。
開発者がSQLiteやSQLの構造を意識しながら、
- SQLの組み立て
- バインド値の管理
- Entity定義
- KSPによる用途別Entity生成
- SQLiteテーブル作成
- SELECT結果からEntityへの変換
- データベース移行
- トランザクション
- SAVEPOINT
- インメモリSQLiteデータベース
- Detektによる静的検証
といった処理を支援します。
例えば、SELECTは次のようにKotlinコードで組み立てます。
val select = Select(ProductSelect::class)
.where {
ProductSelect::enabled eq true
}
.order {
ProductSelect::productCode.asc
}
生成されたSQLはbuild()で取得でき、バインド値も別途参照できます。
val sql = select.build()
val bindValues = select.bindValues
「SQLを見えなくする」のではなく、SQLを意識したまま、文字列連結や定型処理を減らすことを重視しています。
Roomとの違い
RoomとAndrORMは、どちらもAndroidでSQLite開発を支援するためのものですが、考え方が異なります。
| 項目 | AndrORM | Room |
|---|---|---|
| クエリの定義 | KotlinによるSQLビルダー |
@QueryなどにSQLを記述 |
| 動的な検索条件 | Kotlinコードとして段階的に組み立てる | DAOやクエリを用途ごとに構成 |
| SQLの扱い | SQLiteの構造を意識して利用 | DAOを中心に構成 |
| Entity | 手作業またはKSPで生成 | アノテーションを付けて定義 |
| 用途別Entity | KSPでSELECT・DML用途別に生成可能 | EntityやDTOを個別に定義 |
| DB移行 | テーブル再構築とデータ移行を支援 | Migrationを定義 |
| SAVEPOINT | 専用APIを提供 | 主にトランザクション単位 |
| 静的検証 | AndrORM専用Detektルール | Annotation Processor/KSPによる検証 |
AndrORMは、Roomそのものを置き換えることを目的としていません。
Roomの設計が適しているプロジェクトではRoomを使用し、SQLの構造やSQLite固有の動作をより直接的に管理したい場合の選択肢としてAndrORMを開発しています。
どんな開発者を想定しているか
例えば、次のような場合を想定しています。
- SQLの知識を活かしてSQLiteを扱いたい
- 動的な検索条件をKotlinコードで構築したい
- JOINやサブクエリを柔軟に利用したい
- SQL文字列とバインド値の管理を共通化したい
- テーブル定義をKotlinの
data classへ集約したい - SELECT/INSERT/UPDATEなど、用途別にEntityを分けたい
- DBのバージョンアップ時にテーブル再構築やデータ移行を行いたい
- トランザクション内でSAVEPOINTによる部分ロールバックを利用したい
- SQLビルダーの参照ミスを静的解析で検出したい
逆に、SQLをほとんど意識せず、DAO中心で開発したい場合はRoomの方が適していると思います。
主な機能
現在、主に次の機能を実装しています。
- Kotlinの
data classによるテーブル定義 - KSPによるSELECT/INSERT/UPDATEなどの用途別Entity生成
- Entityの手書き定義にも対応
- SELECT/INSERT/UPDATE/DELETE/UPSERT/ABSERT
- INNER JOIN/LEFT JOIN/CROSS JOIN/NATURAL JOIN
- WHERE/HAVING/ORDER BY
- 集約関数または
having指定時のGROUP BY自動生成 - LIMIT/OFFSET
- サブクエリ
- EXISTS/NOT EXISTS
- 集約関数
- スカラー関数
- raw条件/raw式
- SQLとバインド値の一元管理
- Entity/Map/Cursor形式でのSELECT結果取得
- Entity定義からSQLiteテーブルを作成
- DB更新時のテーブル再構築とデータ移行
- トランザクション
- SAVEPOINT
- 独自型とSQLite型の相互変換
- AndrORM専用Detektルール
5~10分で試す
ここからは、最低限の導入方法を紹介します。
1. KSPとDetektを追加
プロジェクトルートのbuild.gradle.ktsへ追加します。
plugins {
id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false
id("io.gitlab.arturbosch.detekt") version "1.23.6" apply false
}
対象モジュールではプラグインを有効にします。
plugins {
id("com.google.devtools.ksp")
id("io.gitlab.arturbosch.detekt")
}
2. AndrORMを追加
対象モジュールのbuild.gradle.ktsへ追加します。
dependencies {
implementation(
"io.github.kawanagare-git:androrm-runtime:0.1.5-alpha"
)
ksp(
"io.github.kawanagare-git:androrm-generator-ksp:0.1.5-alpha"
)
detektPlugins(
"io.github.kawanagare-git:androrm-detekt-rules:0.1.5-alpha"
)
}
3. Entityを定義
例として商品テーブルを定義します。
@Projections(
[
Projection(
entityNameExtend = "Select",
properties = [
ColumnProjection("productCode"),
ColumnProjection("productName"),
ColumnProjection("enabled"),
],
commonInterface = [SELECT],
),
Projection(
entityNameExtend = "Insert",
properties = [
ColumnProjection("productCode"),
ColumnProjection("productName"),
ColumnProjection("enabled"),
],
commonInterface = [INSERT],
),
],
)
@Table(
name = "PRODUCT",
alias = "P",
)
data class Product(
@PrimaryKey
@Column(name = "PRODUCT_CODE")
val productCode: String,
@Column(name = "PRODUCT_NAME")
val productName: String,
@Column(
name = "ENABLED",
default = "1",
)
val enabled: Boolean,
)
@Projectionを使うと、元のEntityから用途別EntityをKSPで生成できます。
上の例では、
Product
├─ ProductSelect
└─ ProductInsert
という用途別Entityを生成して使用できます。
KSPを使わず、必要なアノテーションとマーカーインターフェースを指定してEntityを手書きすることもできます。
4. KSPを実行
Windowsの場合は次のコマンドで生成できます。
.\gradlew.bat :app:kspDebugKotlin
Entity定義や@Projectionを変更し、生成結果を確実に更新したい場合は次のように再実行できます。
.\gradlew.bat :app:kspDebugKotlin --rerun-tasks
5. SELECTを組み立てる
val select = Select(ProductSelect::class)
.where {
ProductSelect::enabled eq true
}
.order {
ProductSelect::productCode.asc
}
SQLとバインド値はそれぞれ取得できます。
val sql = select.build()
val bindValues = select.bindValues
DBヘルパーを利用すると、結果をEntityのリストとして取得できます。
val products =
helper.executeSelectAsEntityList(select)
MapやCursorで取得するAPIも用意しています。
動的な検索条件をKotlinで組み立てる
AndrORMでは、条件をDSLとして組み立てます。
val select = Select(ProductSelect::class)
.where { // 以下の条件は AND 条件となります
ProductSelect::enabled eq true
ProductSelect::productName like "SAMPLE%"
}
.order {
ProductSelect::productCode.asc
}
.limit(50)
.offset(0)
条件に応じてKotlin側で処理を分けられるため、検索画面などで条件数が増減するケースでもSQLビルダーとして組み立てられます。
UPDATE/DELETEの全件操作は明示指定
誤操作防止のため、WHERE句なしでUPDATEやDELETEを実行する場合は、明示的に全件操作を指定します。
UPDATEの場合:
val update = Update(ProductUpdate::class)
.set {
ProductUpdate::enabled assign false
}
.updateAll()
DELETEの場合:
val delete = Delete(ProductDelete::class)
.deleteAll()
意図せずWHERE句を付け忘れたSQLを生成しにくくするための仕様です。
インメモリSQLiteデータベース
0.1.5-alphaでは、AndrOrmDatabaseHelperのdatabaseNameへnullを指定することで、ファイルへ保存しないインメモリSQLiteデータベースも利用できます。
class AppMemoryDatabaseHelper(
context: Context,
) : AndrOrmDatabaseHelper(
context = context,
databaseName = null,
version = 1,
entities = listOf(
Product::class,
),
)
一時的なマスターデータや、アプリ実行中だけ保持したいDBをAndrORMのSQLビルダーやEntity変換と同じ形で扱えます。
SAVEPOINTによる部分ロールバック
AndrORMでは、トランザクションの中でSAVEPOINTを使用できます。
helper.transaction {
executeDml(firstQuery)
val secondResult = savepoint("second_process") {
executeDml(secondQuery)
}
if (secondResult.isSuccess) {
// SAVEPOINT内の処理成功
} else {
// secondQueryはSAVEPOINTまでロールバック済み
}
executeDml(thirdQuery)
}
SAVEPOINT内の通常の処理例外では、
処理失敗
↓
ROLLBACK TO SAVEPOINT
↓
RELEASE SAVEPOINT
↓
SavepointResultを返す
という流れになります。
トランザクション全体ではなく、一部分だけをロールバックしたい処理を想定しています。
Detektによる静的検証
AndrORMには専用のDetektルールも含めています。
現在、主に次のような問題を検出します。
- 定義用エンティティでのテーブル名の重複〔@Table(name=......)〕
- FROM元またはJOIN先に存在しないEntityプロパティの参照
- FROM元とJOIN先で同一Entityを重複指定しているケース
SQLを実行してから気付くのではなく、できる限り開発時に検出することを狙っています。
サンプルアプリ
AndrORMを実際のAndroidアプリへ組み込んだ在庫管理サンプルも公開しています。
サンプルでは、AndrORMだけでなく次の構成も確認できます。
- Jetpack Compose
- ViewModel
- StateFlow
- SharedFlow
- Repository
- AndrORM EntityからUIモデルへの変換
- 商品/商品分類/入出庫履歴
- SELECT/INSERT/UPDATE/UPSERT/ABSERT
- トランザクションによる在庫数と入出庫履歴の整合性確保
「ライブラリ単体のAPI」だけでなく、AndroidアプリのRepository内部へAndrORMを組み込む例として作成しています。
対応環境
この記事で扱っている0.1.5-alphaの主な環境は次のとおりです。
| 項目 | バージョン |
|---|---|
| Kotlin | 1.9.24 |
| Android Gradle Plugin | 8.8.0 |
| Gradle Wrapper | 8.10.2 |
| JDK/JVM Target | 17 |
| compileSdk | 34 |
| targetSdk | 34 |
| minSdk | 24 |
| KSP | 1.9.24-1.0.20 |
| Detekt | 1.23.6 |
現在の位置づけ
AndrORMは現在アルファ版です。
実際に利用できる形で公開していますが、今後もAPIや仕様を変更する可能性があります。
Roomを置き換えるためのライブラリというより、
「SQLiteとSQLを自分で意識して扱いたいAndroid開発者に、もう一つの選択肢を提供する」
ことを目標にしています。
実際に触っていただき、
- APIが分かりにくい
- セットアップで迷う
- このSQLが書きにくい
- この機能が欲しい
- Roomや他のライブラリと比べてここが気になる
といった点があれば、フィードバックをいただけると助かります。
リンク
ライセンスはMIT Licenseです。
本記事で紹介するAndrORMは、2010年代に公開され、2014年頃まで活動していた同名のAndroid向けORM「Androrm」とは別のプロジェクトです。