可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am trying a sample with Room Persistence Library. I created an Entity:
@Entity public class Agent { @PrimaryKey public String guid; public String name; public String email; public String password; public String phone; public String licence; }
Created a DAO class:
@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); }
Created the Database class:
@Database(entities = {Agent.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract AgentDao agentDao(); }
Exposed database using below subclass in 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() } }
Implemented below function in my activity:
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(); } }
Unfortunately on execution of above method it crashes with below stack trace:
Seems like that problem is related to execution of db operation on main thread. However the sample test code provided in above link does not run on a separate thread:
@Test public void writeUserAndReadInList() throws Exception { User user = TestUtil.createUser(3); user.setName("george"); mUserDao.insert(user); List byName = mUserDao.findUsersByName("george"); assertThat(byName.get(0), equalTo(user)); }
Am I missing anything over here? How can I make it execute without crash? Please suggest.
回答1:
Database access on main thread locking the UI is the error, like Dale said.
Something like the following code should work inside the signUpAction(View view) method...
final AgentDao agentDao = MyApp.DatabaseSetup.getDatabase().agentDao(); new AsyncTask() { @Override protected Integer doInBackground(Void... params) { return agentDao.agentsCount(email, phone, license); } @Override protected void onPostExecute(Integer agentsCount) { if (agentsCount > 0) { //2: If it already exists then prompt user Toast.makeText(Activity.this, "Agent already exists!", Toast.LENGTH_LONG).show(); } else { Toast.makeText(Activity.this, "Agent does not exist! Hurray :)", Toast.LENGTH_LONG).show(); onBackPressed(); } } }.execute();
*Replace Activity.this with [your activity].this
Or creating a new class extending AsyncTask would be cleaner.
Also, your question about the Google's test example... They state in that web page:
The recommended approach for testing your database implementation is writing a JUnit test that runs on an Android device. Because these tests don't require creating an activity, they should be faster to execute than your UI tests.
No Activity, no UI.
--EDIT--
For people wondering... You have other options. I recommend taking a look into the new ViewModel and LiveData components. LiveData works great with Room. https://developer.android.com/topic/libraries/architecture/livedata.html
Another option is the RxJava/RxAndroid. More powerful but more complex than LiveData. https://github.com/ReactiveX/RxJava
回答2:
It's not recommended but you can access to database on main thread with allowMainThreadQueries()
MyApp.database = Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()
回答3:
For all the RxJava or RxAndroid or RxKotlin lovers out there
Observable.just(db) .subscribeOn(Schedulers.io()) .subscribe { db -> // database operation }
回答4:
Straight-forward code using Kotlin Coroutines
AsyncTask is really clunky. Kotlin coroutines is a cleaner alternative (essentially just your synchronous code with a couple of extra keywords).
gradle.properties:
# Coroutines opt-in kotlin.coroutines=enable
UI thread (non-blocking):
private fun myFun() { launch(UI) { val query = async(CommonPool) { // Async stuff MyApp.DatabaseSetup.database.agentDao().agentsCount(email, phone, license) } val agentsCount = query.await() // do UI stuff } }
The suspend
keyword ensures async methods are only called from within async blocks, however (as noted by @Robin) this doesn't play nicely with Room annotated methods.
// Wrap API to use suspend (probably not worth it) public suspend fun agentsCount(...): Int = agentsCountPrivate(...) @Query("SELECT ...") protected abstract fun agentsCountPrivate(...): Int
回答5:
With the Jetbrains Anko library, you can use the doAsync{..} method to automatically execute database calls. This takes care of the verbosity problem you seemed to have been having with mcastro's answer.
Example usage:
doAsync { Application.database.myDAO().insertUser(user) }
I use this frequently for inserts and updates, however for select queries I reccommend using the RX workflow.
回答6:
The error message,
Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.
Is quite descriptive and accurate. The question is how should you avoid accessing the database on the main thread. That is a huge topic, but to get started, read about AsyncTask (click here)
-----EDIT----------
I see you are having problems when you run a unit test. You have a couple of choices to fix this:
Run the test directly on the development machine rather than on an Android device (or emulator). This works for tests that are database-centric and don't really care whether they are running on a device.
Use the annotation @RunWith(AndroidJUnit4.class)
to run the test on the android device, but not in an activity with a UI. More details about this can be found in this tutorial
回答7:
You cannot run it on main thread instead use handlers, async or working threads . A sample code is available here and read article over room library here : Android's Room Library
/** * 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(); } });
If you want to run it on main thread which is not preferred way .
You can use this method to achieve on main thread Room.inMemoryDatabaseBuilder()
回答8:
For quick queries you can allow room to execute it on UI thread.
AppDatabase db = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME).allowMainThreadQueries().build();
In my case I had to figure out of the clicked user in list exists in database or not. If not then create the user and start another activity
@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 users = new ArrayList(); users.add(user); List 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); } }
回答9:
An elegant RxJava/Kotlin solution is to use Completable.fromCallable
, which will give you an Observable which does not return a value, but can observed and subscribed on a different thread.
public Completable insert(Event event) { return Completable.fromCallable(new Callable() { @Override public Void call() throws Exception { return database.eventDao().insert(event) } } }
Or in Kotlin:
fun insert(event: Event) : Completable = Completable.fromCallable { database.eventDao().insert(event) }
You can the observe and subscribe as you would usually:
dataManager.insert(event) .subscribeOn(scheduler) .observeOn(AndroidSchedulers.mainThread()) .subscribe(...)
回答10:
You can allow database access on the main thread but only for debugging purpose, you shouldn't do this on production.
Here is the reason.
Note: Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries―queries that return instances of LiveData or Flowable―are exempt from this rule because they asynchronously run the query on a background thread when needed.
回答11:
You can't access the database directly on the main thread, for example:
public void add(MyEntity item) { appDatabase.myDao().add(item); }
You have to extend AsyncTask for update, add, and delete in the ViewModel.
Example:
public class MyViewModel extends AndroidViewModel { private LiveData> list; private AppDatabase appDatabase; public MyViewModel(Application application) { super(application); appDatabase = AppDatabase.getDatabase(this.getApplication()); list = appDatabase.myDao().getItems(); } public LiveData> getItems() { return list; } public void delete(Obj item) { new deleteAsyncTask(appDatabase).execute(item); } private static class deleteAsyncTask extends AsyncTask { 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 { private AppDatabase db; addAsyncTask(AppDatabase appDatabase) { db = appDatabase; } @Override protected Void doInBackground(final MyEntity... params) { db.myDao().add((params[0])); return null; } } }
回答12:
If you are more comfortable with Async task:
new AsyncTask() { @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();