Androidアプリ開発でデータベースを使う機会は少なくないので、DB接続のサンプルとしてRoomの使用例をまとめます。
今回作ったのはこのようなアプリです。起動するとボタンが表示され・・
ボタンをクリックするたびに、クリック時のタイムスタンプが追加されていく、というものです。
ボタンをクリックすることで、まずクリック時のタイムスタンプをテーブルに格納し、その後それまで格納された全てのレコードを取得してTextViewに格納しています。
基本的にGoogleの公式ドキュメントに従って、Database, Entity, Daoを作成していきますが、DatabaseはSingletonで生成するとか、DBアクセスはUIスレッドとは別スレッドで実行しないといけないとか、制約がいろいろあります。
目次
build.gradleへ依存関係の追加
まずはbuild.gradleに、Roomを使用するための依存関係を追加します。
dependencies { def room_version = "2.2.6" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" : }
画面レイアウトの作成
続いて画面レイアウトです。ここはButtonと、データ表示用のTextViewが並んだシンプルなもの。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id = "@+id/index" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button" android:layout_width="93dp" android:layout_height="48dp" android:text="Button" app:layout_constraintBottom_toTopOf="@+id/index" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Entityの作成
Entityです。主キーやカラムを示すためのアノテーションを付けます。
import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class AccessTime { @PrimaryKey(autoGenerate = true) private int id; @ColumnInfo(name = "access_time") private String accessTime; public AccessTime(String accessTime) { this.accessTime = accessTime; } public void setId(int id) { this.id = id; } public int getId() { return id; } public void setAccessTime(String accessTime) { this.accessTime = accessTime; } public String getAccessTime() { return accessTime; } }
Daoの作成
Daoを作成します。interfaceとして作成し、メソッドを定義しておきます。
import java.util.List; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; @Dao public interface AccessTimeDao { @Query("SELECT * FROM accesstime") List<AccessTime> getAll(); @Query("SELECT * FROM accesstime WHERE id IN (:ids)") List<AccessTime> loadAllByIds(int[] ids); @Insert void insertAll(AccessTime... accessTimes); @Insert void insert(AccessTime accessTime); @Delete void delete(AccessTime accessTime); }
Databaseの作成
続いてDatabaseの作成です。RoomDatabaseを継承したabstractクラスになります。
import androidx.room.Database; import androidx.room.RoomDatabase; @Database(entities = {AccessTime.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract AccessTimeDao accessTimeDao(); }
Database呼び出し用Singletonクラスの作成
公式ページに以下の記載があるので、それに従ってSingletonクラスを作成します。
注: シングル プロセスで実行するアプリの場合は、
AppDatabase
オブジェクトをインスタンス化する際にシングルトン設計パターンに従ってください。各RoomDatabase
インスタンスは非常に高コストであり、単一のプロセス内で複数のインスタンスにアクセスする必要はほとんどありません。
import android.content.Context; import androidx.room.Room; public class AppDatabaseSingleton { private static AppDatabase instance = null; public static AppDatabase getInstance(Context context) { if (instance != null) { return instance; } instance = Room.databaseBuilder(context, AppDatabase.class, "database-name").build(); return instance; } }
Activityの作成
最後にActivityを整えます。
Activityの大まかな構成は次のようなものです。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // データベースを呼び出す : // ボタンクリック時のListenerをセットする : } // ボタンクリック時のListenerクラス private class ButtonClickListener implements View.OnClickListener { @Override public void onClick(View view) { // 非同期のデータベースアクセスクラスをコールする : } } // 非同期でデータベースにアクセスするクラス private static class DataStoreAsyncTask extends AsyncTask<Void, Void, Integer> { @Override protected Integer doInBackground(Void... params) { // タイムスタンプを生成する処理 : // 生成したタイムスタンプを格納する処理 : // これまで生成したタイムスタンプを取得し文字列として連結する処理 : } @Override protected void onPostExecute(Integer code) { // 表示したい文字列をTextViewにセットする処理 : } } }
DBへの非同期アクセス
RoomDatabaseの呼び出し処理は、UIと同じスレッドで実行するとエラーが発生します。
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
databaseの呼び出しはUIを長時間ロックする恐れがあるので、非同期で実行することが推奨されているようです。
今回は非同期アクセスの手段として、AsyncTaskを継承したinnerクラスを利用します。
private static class DataStoreAsyncTask extends AsyncTask<Void, Void, Integer> { private WeakReference<Activity> weakActivity; private AppDatabase db; private TextView textView; private StringBuilder sb; public DataStoreAsyncTask(AppDatabase db, Activity activity, TextView textView) { this.db = db; weakActivity = new WeakReference<>(activity); this.textView = textView; } @Override protected Integer doInBackground(Void... params) { AccessTimeDao accessTimeDao = db.accessTimeDao(); accessTimeDao.insert(new AccessTime(new Timestamp(System.currentTimeMillis()).toString())); sb = new StringBuilder(); List<AccessTime> atList = accessTimeDao.getAll(); for (AccessTime at: atList) { sb.append(at.getAccessTime()).append("\n"); } return 0; } @Override protected void onPostExecute(Integer code) { Activity activity = weakActivity.get(); if(activity == null) { return; } textView.setText(sb.toString()); } }
上の概要で書いたように、この中でデータベースへのアクセス(データの格納・取得)と、TextViewへのセットを行っています。
今回はJavaを使用していますが、KotlinであればRoomがサポートしているCoroutineを使えるらしい。。
全体サンプルコード
他の処理も追加した全体のActivityコードは次のようになります。
import androidx.appcompat.app.AppCompatActivity; import androidx.room.Room; import androidx.room.RoomDatabase; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.lang.ref.WeakReference; import java.sql.Timestamp; import java.util.List; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.index); Button bt = findViewById(R.id.button); AppDatabase db = AppDatabaseSingleton.getInstance(getApplicationContext()); bt.setOnClickListener(new ButtonClickListener(this, db, tv)); } private class ButtonClickListener implements View.OnClickListener { private Activity activity; private AppDatabase db; private TextView tv; private ButtonClickListener(Activity activity, AppDatabase db, TextView tv) { this.activity = activity; this.db = db; this.tv = tv; } @Override public void onClick(View view) { new DataStoreAsyncTask(db, activity, tv).execute(); } } private static class DataStoreAsyncTask extends AsyncTask<Void, Void, Integer> { private WeakReference<Activity> weakActivity; private AppDatabase db; private TextView textView; private StringBuilder sb; public DataStoreAsyncTask(AppDatabase db, Activity activity, TextView textView) { this.db = db; weakActivity = new WeakReference<>(activity); this.textView = textView; } @Override protected Integer doInBackground(Void... params) { AccessTimeDao accessTimeDao = db.accessTimeDao(); accessTimeDao.insert(new AccessTime(new Timestamp(System.currentTimeMillis()).toString())); sb = new StringBuilder(); List<AccessTime> atList = accessTimeDao.getAll(); for (AccessTime at: atList) { sb.append(at.getAccessTime()).append("\n"); } return 0; } @Override protected void onPostExecute(Integer code) { Activity activity = weakActivity.get(); if(activity == null) { return; } textView.setText(sb.toString()); } } }
まとめ
Roomを使用する際は、次の点に注意しながら作成していけば良いことがわかりました。
- Roomを使用する場合は、主にEntity, Dao, Databaseの3種類のクラスで構成する
- RoomDatabaseはSingletonで生成する
- RoomDatabaseへのアクセス処理は、メインのスレッドとは別スレッドで行う