私は Room Persistence Library でサンプルを試しています。私はエンティティを作成しました:
@Entity
public class Agent {
@PrimaryKey
public String guid;
public String name;
public String email;
public String password;
public String phone;
public String licence;
}
DAOクラスを作成しました:
@Dao
public interface AgentDao {
@Query("SELECT COUNT(*) FROM Agent where email = :email OR phone = :phone OR licence = :licence")
int agentsCount(String email, String phone, String licence);
@Insert
void insertAgent(Agent agent);
}
Databaseクラスを作成しました。
@Database(entities = {Agent.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AgentDao agentDao();
}
Kotlinで以下のサブクラスを使用してデータベースを公開しました。
class MyApp : Application() {
companion object DatabaseSetup {
var database: AppDatabase? = null
}
override fun onCreate() {
super.onCreate()
MyApp.database = Room.databaseBuilder(this, AppDatabase::class.Java, "MyDatabase").build()
}
}
私の活動で以下の機能を実装しました:
void signUpAction(View view) {
String email = editTextEmail.getText().toString();
String phone = editTextPhone.getText().toString();
String license = editTextLicence.getText().toString();
AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
//1: Check if agent already exists
int agentsCount = agentDao.agentsCount(email, phone, license);
if (agentsCount > 0) {
//2: If it already exists then Prompt user
Toast.makeText(this, "Agent already exists!", Toast.LENGTH_LONG).show();
}
else {
Toast.makeText(this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
onBackPressed();
}
}
残念ながら、上記のメソッドを実行すると、以下のスタックトレースでクラッシュします。
FATAL EXCEPTION: main
Process: com.example.me.MyApp, PID: 31592
Java.lang.IllegalStateException: Could not execute method for Android:onClick
at Android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.Java:293)
at Android.view.View.performClick(View.Java:5612)
at Android.view.View$PerformClick.run(View.Java:22288)
at Android.os.Handler.handleCallback(Handler.Java:751)
at Android.os.Handler.dispatchMessage(Handler.Java:95)
at Android.os.Looper.loop(Looper.Java:154)
at Android.app.ActivityThread.main(ActivityThread.Java:6123)
at Java.lang.reflect.Method.invoke(Native Method)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:867)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:757)
Caused by: Java.lang.reflect.InvocationTargetException
at Java.lang.reflect.Method.invoke(Native Method)
at Android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.Java:288)
at Android.view.View.performClick(View.Java:5612)
at Android.view.View$PerformClick.run(View.Java:22288)
at Android.os.Handler.handleCallback(Handler.Java:751)
at Android.os.Handler.dispatchMessage(Handler.Java:95)
at Android.os.Looper.loop(Looper.Java:154)
at Android.app.ActivityThread.main(ActivityThread.Java:6123)
at Java.lang.reflect.Method.invoke(Native Method)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:867)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:757)
Caused by: Java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.
at Android.Arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.Java:137)
at Android.Arch.persistence.room.RoomDatabase.query(RoomDatabase.Java:165)
at com.example.me.MyApp.RoomDb.Dao.AgentDao_Impl.agentsCount(AgentDao_Impl.Java:94)
at com.example.me.MyApp.View.SignUpActivity.signUpAction(SignUpActivity.Java:58)
at Java.lang.reflect.Method.invoke(Native Method)
at Android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.Java:288)
at Android.view.View.performClick(View.Java:5612)
at Android.view.View$PerformClick.run(View.Java:22288)
at Android.os.Handler.handleCallback(Handler.Java:751)
at Android.os.Handler.dispatchMessage(Handler.Java:95)
at Android.os.Looper.loop(Looper.Java:154)
at Android.app.ActivityThread.main(ActivityThread.Java:6123)
at Java.lang.reflect.Method.invoke(Native Method)
at com.Android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.Java:867)
at com.Android.internal.os.ZygoteInit.main(ZygoteInit.Java:757)
この問題はメインスレッドでのdb操作の実行に関連しているようです。ただし、上記のリンクで提供されているサンプルテストコードは別のスレッドでは実行されません。
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
ここに何か足りないの?クラッシュせずに実行させるにはどうすればいいですか?提案してください。
Daleが言ったように、UIをロックしているメインスレッド上のデータベースアクセスはエラーです。
AsyncTaskを拡張するActivityに静的なネストクラスを作成します(メモリリークを防ぐため)。
private static class AgentAsyncTask extends AsyncTask<Void, Void, Integer> {
//Prevent leak
private WeakReference<Activity> weakActivity;
private String email;
private String phone;
private String license;
public AgentAsyncTask(Activity activity, String email, String phone, String license) {
weakActivity = new WeakReference<>(activity);
this.email = email;
this.phone = phone;
this.license = license;
}
@Override
protected Integer doInBackground(Void... params) {
AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao();
return agentDao.agentsCount(email, phone, license);
}
@Override
protected void onPostExecute(Integer agentsCount) {
Activity activity = weakActivity.get();
if(activity == null) {
return;
}
if (agentsCount > 0) {
//2: If it already exists then Prompt user
Toast.makeText(activity, "Agent already exists!", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(activity, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show();
activity.onBackPressed();
}
}
}
あるいは、独自のファイルに最終クラスを作成することもできます。
それからsignUpAction(View view)メソッドでそれを実行します。
new AgentAsyncTask(this, email, phone, license).execute();
場合によっては、アクティビティ内のAgentAsyncTaskへの参照を保持して、アクティビティが破棄されたときにキャンセルできるようにすることもできます。しかし、あなたは自分でトランザクションを中断しなければならないでしょう。
また、グーグルのテスト例についてのあなたの質問...彼らはそのウェブページで述べています:
データベース実装をテストするための推奨されるアプローチは、Androidデバイス上で動作するJUnitテストを書くことです。これらのテストはアクティビティを作成する必要がないので、UIテストより実行が速いはずです。
アクティビティなし、UIなし.
- 編集 -
疑問に思う人のために...あなたには他の選択肢があります。新しいViewModelコンポーネントとLiveDataコンポーネントを調べてみることをお勧めします。 LiveDataはRoomとうまく連携します。 https://developer.Android.com/topic/libraries/architecture/livedata.html
別の選択肢はRxJava/RxAndroidです。 LiveDataよりも強力ですが複雑です。 https://github.com/ReactiveX/RxJava
- 編集2 -
多くの人がこの答えに出くわすかもしれないので...今日最も一般的に言えば、最良の選択肢はKotlin Coroutinesです。 Roomは直接サポートしています(現在はベータ版)。 https://kotlinlang.org/docs/reference/coroutines-overview.htmlhttps:// developer。 Android.com/jetpack/androidx/releases/room#2.1.0-beta01
お勧めしませんが、allowMainThreadQueries()
を使用してメインスレッドでデータベースにアクセスできます。
MyApp.database = Room.databaseBuilder(this, AppDatabase::class.Java, "MyDatabase").allowMainThreadQueries().build()
AsyncTaskは本当に不格好です。 Kotlinコルーチンは、よりクリーンな代替方法です(基本的には同期コードと2つのキーワードだけです)。
private fun myFun() {
launch { // coroutine on Main
val query = async(Dispatchers.IO) { // coroutine on IO
MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license)
}
val agentsCount = query.await()
// do UI stuff
}
}
そしてそれだけです!!
アクティビティからの非同期を使用するには、CoroutineScopeが必要です。あなたはこのようにあなたの活動を使うことができます:
class LoadDataActivity : AppCompatActivity(), CoroutineScope {
private val job by lazy { Job() }
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onDestroy() {
super.onDestroy()
job.cancel() // cancels all coroutines under this scope
}
// ...rest of class
}
2019年5月8日:Room 2.1でsuspend
( https://youtu.be/Qxj2eBmXLHg?t=1662 )
suspend
キーワードは、非同期メソッドが非同期ブロック内からのみ呼び出されることを保証しますが、(@Robinで指摘されているように)これはRoom(<2.1)アノテーション付きメソッドではうまく機能しません。
// Wrap API to use suspend (probably not worth it)
public suspend fun agentsCount(...): Int = agentsCountPrivate(...)
@Query("SELECT ...")
protected abstract fun agentsCountPrivate(...): Int
メインスレッドで実行することはできません。代わりに、ハンドラ、非同期スレッド、または作業スレッドを使用してください。サンプルコードはこちらから入手でき、ここでルームライブラリに関する記事を読むことができます。 Androidのルームライブラリ
/**
* Insert and get data using Database Async way
*/
AsyncTask.execute(new Runnable() {
@Override
public void run() {
// Insert Data
AppDatabase.getInstance(context).userDao().insert(new User(1,"James","Mathew"));
// Get Data
AppDatabase.getInstance(context).userDao().getAllUsers();
}
});
あなたがメインスレッドでそれを実行したいなら、これは好ましい方法ではありません。
このメソッドを使ってメインスレッドで実行することができますRoom.inMemoryDatabaseBuilder()
Jetbrains Ankoライブラリでは、doAsync {..}メソッドを使用して自動的にデータベース呼び出しを実行できます。これは、mcastroの答えにあったと思われる冗長性の問題を解決します。
使用例
doAsync {
Application.database.myDAO().insertUser(user)
}
私はこれを挿入と更新に頻繁に使用しますが、RXワークフローを使用して推奨する選択クエリには使用します。
洗練されたRxJava/Kotlinの解決策は Completable.fromCallable
を使うことです。糸。
public Completable insert(Event event) {
return Completable.fromCallable(new Callable<Void>() {
@Override
public Void call() throws Exception {
return database.eventDao().insert(event)
}
}
}
またはコトリンで:
fun insert(event: Event) : Completable = Completable.fromCallable {
database.eventDao().insert(event)
}
通常どおり、観察して購読することができます。
dataManager.insert(event)
.subscribeOn(scheduler)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...)
エラーメッセージ
メインスレッド上のデータベースにアクセスすることはできません。長期間にわたってUIをロックする可能性があるためです。
かなり説明的で正確です。問題は、メインスレッドでデータベースにアクセスすることをどのように避けるべきかということです。これは大きなトピックですが、始めるには AsyncTaskについて(こちらをクリック) を読んでください。
-----編集----------
ユニットテストを実行すると問題が発生しているようです。これを修正するにはいくつかの選択肢があります。
Androidデバイス(またはエミュレータ)ではなく、開発用マシンで直接テストを実行してください。これはデータベース中心であり、それらがデバイス上で実行されているかどうかを実際には気にしないテストに有効です。
Androidデバイス上でテストを実行するにはアノテーション@RunWith(AndroidJUnit4.class)
を使用しますが、UIのあるアクティビティでは使用しません。これについての詳細は、このチュートリアルで見つけることができます
AsyncTaskで実行するのが簡単なラムダ
AsyncTask.execute(() -> //run your query here );
非同期タスクに慣れている場合:
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... voids) {
return Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
.getRecordingDAO()
.getAll()
.size();
}
@Override
protected void onPostExecute(Integer integer) {
super.onPostExecute(integer);
Toast.makeText(HomeActivity.this, "Found " + integer, Toast.LENGTH_LONG).show();
}
}.execute();
バックグラウンドでリクエストを実行する必要があります。簡単な方法は エグゼキュータ を使うことです。
Executors.newSingleThreadExecutor().execute {
yourDb.yourDao.yourRequest() //Replace this by your request
}
それを解決するためにこのコードを使うことができます。
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
appDb.daoAccess().someJobes();//replace with your code
}
});
あるいはラムダでは、このコードを使うことができます:
Executors.newSingleThreadExecutor().execute(() -> appDb.daoAccess().someJobes());
appDb.daoAccess().someJobes()
を自分のコードに置き換えることができます。
更新:DAO内で@RawQueryとSupportSQLiteQueryを使ってクエリを作成しようとしたときにもこのメッセージが表示されました。
@Transaction
public LiveData<List<MyEntity>> getList(MySettings mySettings) {
//return getMyList(); -->this is ok
return getMyList(new SimpleSQLiteQuery("select * from mytable")); --> this is an error
解決策:ViewModel内でクエリを作成してDAOに渡します。
public MyViewModel(Application application) {
...
list = Transformations.switchMap(searchParams, params -> {
StringBuilder sql;
sql = new StringBuilder("select ... ");
return appDatabase.rawDao().getList(new SimpleSQLiteQuery(sql.toString()));
});
}
それとも….
メインスレッドでデータベースに直接アクセスしないでください。たとえば、
public void add(MyEntity item) {
appDatabase.myDao().add(item);
}
更新、追加、削除の操作にはAsyncTaskを使用する必要があります。
例:
public class MyViewModel extends AndroidViewModel {
private LiveData<List<MyEntity>> list;
private AppDatabase appDatabase;
public MyViewModel(Application application) {
super(application);
appDatabase = AppDatabase.getDatabase(this.getApplication());
list = appDatabase.myDao().getItems();
}
public LiveData<List<MyEntity>> getItems() {
return list;
}
public void delete(Obj item) {
new deleteAsyncTask(appDatabase).execute(item);
}
private static class deleteAsyncTask extends AsyncTask<MyEntity, Void, Void> {
private AppDatabase db;
deleteAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
@Override
protected Void doInBackground(final MyEntity... params) {
db.myDao().delete((params[0]));
return null;
}
}
public void add(final MyEntity item) {
new addAsyncTask(appDatabase).execute(item);
}
private static class addAsyncTask extends AsyncTask<MyEntity, Void, Void> {
private AppDatabase db;
addAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
@Override
protected Void doInBackground(final MyEntity... params) {
db.myDao().add((params[0]));
return null;
}
}
}
選択操作にLiveDataを使用する場合は、AsyncTaskは必要ありません。
データベース操作は別のスレッドで実行するだけです。こんな感じ(コトリン):
Thread {
//Do your database´s operations here
}.start()
素早い問い合わせのために、UIスレッドでそれを実行するためのスペースを確保することができます。
AppDatabase db = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();
私の場合は、リスト内のクリックされたユーザーがデータベースに存在するかどうかを把握する必要がありました。そうでない場合は、ユーザーを作成して別のアクティビティを開始します
@Override
public void onClick(View view) {
int position = getAdapterPosition();
User user = new User();
String name = getName(position);
user.setName(name);
AppDatabase appDatabase = DatabaseCreator.getInstance(mContext).getDatabase();
UserDao userDao = appDatabase.getUserDao();
ArrayList<User> users = new ArrayList<User>();
users.add(user);
List<Long> ids = userDao.insertAll(users);
Long id = ids.get(0);
if(id == -1)
{
user = userDao.getUser(name);
user.setId(user.getId());
}
else
{
user.setId(id);
}
Intent intent = new Intent(mContext, ChatActivity.class);
intent.putExtra(ChatActivity.EXTRAS_USER, Parcels.wrap(user));
mContext.startActivity(intent);
}
}
メインスレッドでデータベースアクセスを許可することができますが、デバッグ目的でのみ、本番環境では行わないでください。
注:ビルダーでallowMainThreadQueries()を呼び出さない限り、Roomはメインスレッドでのデータベースアクセスをサポートしません。これは、UIを長期間ロックする可能性があるためです。非同期クエリ(LiveDataまたはFlowableのインスタンスを返すクエリ)は、必要に応じてバックグラウンドスレッドで非同期にクエリを実行するため、この規則から除外されます。
あなたはFutureとCallableを使うことができます。そのため、長いasynctaskを書く必要はなく、allowMainThreadQueries()を追加せずにクエリを実行できます。
私のdaoクエリ: -
@Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();
私のリポジトリメソッド: -
public UserData getDefaultData() throws ExecutionException, InterruptedException {
Callable<UserData> callable = new Callable<UserData>() {
@Override
public UserData call() throws Exception {
return userDao.getDefaultData();
}
};
Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);
return future.get();
}
handlerThreadを使って別のスレッドですべてのデータベースアクセスを順番に処理することができます。詳しくは handlerThread を使って確認してください。
基本的には、handlerThreadを拡張したクラスを作成し、次に示すようにonLooperPreparedをオーバーライドします。
public class MyHandlerThread extends HandlerThread{
private Handler handler;
public SocketManager(){
super("MyHandlerThread");
}
@Override
protected void onLooperPrepared() {
handler = new Handler(getLooper()) {
@Override
public void handleMessage(Android.os.Message msg) {
super.handleMessage(msg);
//handle all your database tasks here
YourObject yourObject = (YourObject)msg.obj;
}};
}
//send message to the background thread
public void sendMessage(YourObject msg){
Android.os.Message msg = new Android.os.Message();
msg.obj = message;
handler.sendMessage(msg);
}
public void onDestroy(){
quit();
interrupt();
}
}
次に、アクティビティクラスに次のコードを追加します。
MyHandlerThread myHandlerThread = new MyHandlerThread();
myHandlerThread.start();
スレッドと通信するためには、myHandlerThread.sendMessage()
を呼び出し、必要なデータを渡します。
メインスレッドとバックグラウンドスレッド間の双方向通信を行うためには、メインスレッドから別のハンドラを使用してメッセージを送信することで詳細を確認できます 双方向通信