Android Room - แบบสอบถามแบบเลือกง่าย - ไม่สามารถเข้าถึงฐานข้อมูลในเธรดหลัก


126

ฉันพยายามที่ตัวอย่างมีห้องคงทนห้องสมุด ฉันสร้างเอนทิตี:

@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(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));
    }

ฉันพลาดอะไรตรงนี้ไปหรือเปล่า? ฉันจะทำให้มันดำเนินการโดยไม่ผิดพลาดได้อย่างไร? ช่วยแนะนำหน่อยครับ.


1
แม้ว่าจะเขียนเพื่อ Kotlin แต่บทความนี้อธิบายปัญหาพื้นฐานได้ดีมาก!
Peter Lehnhardt


ดูคำตอบนี้ คำตอบนี้ใช้ได้กับฉันstackoverflow.com/a/51720501/7655085
Somen Tushir

คำตอบ:


59

การเข้าถึงฐานข้อมูลบนเธรดหลักที่ล็อก UI คือข้อผิดพลาดดังที่ Dale กล่าว

สร้างคลาสซ้อนแบบคงที่ (เพื่อป้องกันการรั่วไหลของหน่วยความจำ) ในกิจกรรมของคุณที่ขยาย AsyncTask

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 ในกิจกรรมของคุณเพื่อให้คุณสามารถยกเลิกได้เมื่อกิจกรรมถูกทำลาย แต่คุณจะต้องขัดขวางการทำธุรกรรมด้วยตัวเอง

นอกจากนี้คำถามของคุณเกี่ยวกับตัวอย่างการทดสอบของ Google ... พวกเขาระบุไว้ในหน้าเว็บนั้น:

แนวทางที่แนะนำสำหรับการทดสอบการใช้งานฐานข้อมูลของคุณคือการเขียนการทดสอบ JUnit ที่ทำงานบนอุปกรณ์ Android เนื่องจากการทดสอบเหล่านี้ไม่จำเป็นต้องสร้างกิจกรรมจึงควรดำเนินการได้เร็วกว่าการทดสอบ UI ของคุณ

ไม่มีกิจกรรมไม่มี UI

--EDIT--

สำหรับคนที่สงสัย ... คุณมีทางเลือกอื่น ฉันขอแนะนำให้ดูส่วนประกอบ 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.html https://developer.android.com/jetpack/androidx/releases/room#2.1.0-beta01


31
ฉันควรจะสร้างงาน async ขนาดใหญ่นี้ (ที่คุณเสนอในตัวอย่างโค้ด) ทุกครั้งที่ฉันต้องการเข้าถึงฐานข้อมูลหรือไม่? รหัสเป็นโหลหรือมากกว่านั้นแทนที่จะเป็นบรรทัดเดียวเพื่อรับข้อมูลบางส่วนจาก db คุณเสนอให้สร้างคลาสใหม่ด้วย แต่หมายความว่าฉันต้องสร้างคลาส AsyncTask ใหม่สำหรับการเรียกฐานข้อมูลแทรก / เลือกแต่ละครั้งหรือไม่
Piotrek

7
คุณมีทางเลือกอื่นใช่ คุณอาจต้องการดูส่วนประกอบ ViewModel และ LiveData ใหม่ เมื่อใช้ LiveData คุณไม่จำเป็นต้องใช้ AsyncTask วัตถุจะได้รับการแจ้งเตือนทุกครั้งที่มีการเปลี่ยนแปลง developer.android.com/topic/libraries/architecture/... developer.android.com/topic/libraries/architecture/... นอกจากนี้ยังมี AndroidRx (แม้ว่ามันจะสวยมากสิ่งที่ LiveData ไม่) และสัญญา เมื่อใช้ AsyncTask คุณสามารถสร้างสถาปัตยกรรมในลักษณะที่คุณสามารถรวมการดำเนินการหลายอย่างไว้ใน AsyncTask เดียวหรือแยกกัน
mcastro

@Piotrek - ตอนนี้ Kotlin มีการอบแบบ async แล้ว (แม้ว่าจะมีการตั้งค่าสถานะทดลอง) ดูคำตอบของฉันซึ่งค่อนข้างเล็กน้อย คำตอบของ Samuel Robert ครอบคลุม Rx ฉันไม่เห็นคำตอบ LiveData ที่นี่ แต่นั่นอาจเป็นทางเลือกที่ดีกว่าหากคุณต้องการให้สังเกตได้
AjahnCharles

การใช้ AsyncTask ปกติดูเหมือนจะไม่ได้ผลในขณะนี้ แต่ยังคงได้รับข้อยกเว้นของรัฐที่ผิดกฎหมาย
Peterstev Uremgba

@Piotrek คุณกำลังบอกฉันว่าคุณใช้เพื่อเรียกใช้การเข้าถึงฐานข้อมูลบนเธรดหลักจากประสบการณ์ทั้งหมดของคุณ?
mr5

142

ไม่แนะนำ แต่คุณสามารถเข้าถึงฐานข้อมูลบนเธรดหลักด้วยไฟล์ allowMainThreadQueries()

MyApp.database =  Room.databaseBuilder(this, AppDatabase::class.java, "MyDatabase").allowMainThreadQueries().build()

10
Room ไม่อนุญาตให้เข้าถึงฐานข้อมูลบนเธรดหลักเว้นแต่คุณจะเรียกallowMainThreadQueries()ใช้ตัวสร้างเนื่องจากอาจล็อก UI เป็นเวลานาน แบบสอบถามแบบอะซิงโครนัส (แบบสอบถามที่ส่งคืนLiveDataหรือ RxJava Flowable) ได้รับการยกเว้นจากกฎนี้เนื่องจากจะเรียกใช้แบบสอบถามบนเธรดพื้นหลังแบบอะซิงโครนัสเมื่อจำเป็น
pRaNaY

4
ขอบคุณสิ่งนี้มีประโยชน์มากสำหรับการโยกย้ายเนื่องจากฉันต้องการทดสอบห้องทำงานตามที่คาดไว้ก่อนที่จะแปลงจาก Loaders เป็น Live Data
SammyT

5
@JideGuruTheProgrammer ไม่มันไม่ควร ในบางกรณีอาจทำให้แอปของคุณช้าลงมาก การดำเนินการควรทำแบบอะซิงโครนัส
Alex

@ อเล็กซ์ไม่เคยมีกรณีสำหรับการสอบถามในเธรดหลัก?
Justin Meiners

2
@JustinMeiners เป็นเพียงการปฏิบัติที่ไม่ดีคุณสามารถทำได้ตราบเท่าที่ฐานข้อมูลยังมีขนาดเล็ก
lasec0203

54

Kotlin Coroutines (ชัดเจนและรัดกุม)

AsyncTask เป็นบ้าจริงๆ Coroutines เป็นทางเลือกที่สะอาดกว่า (เพียงแค่โรยคำหลักสองสามคำและรหัสการซิงค์ของคุณจะกลายเป็น async)

// Step 1: add `suspend` to your fun
suspend fun roomFun(...): Int
suspend fun notRoomFun(...) = withContext(Dispatchers.IO) { ... }

// Step 2: launch from coroutine scope
private fun myFun() {
    lifecycleScope.launch { // coroutine on Main
        val queryResult = roomFun(...) // coroutine on IO
        doStuff() // ...back on Main
    }
}

การพึ่งพา (เพิ่มขอบเขตโครูทีนสำหรับส่วนประกอบโค้ง):

// lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha04'

// viewModelScope:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha04'

- อัปเดต:
08- พ.ค. -2562: ขณะนี้ห้อง 2.1รองรับsuspend
13 ก.ย. 2562:อัปเดตเพื่อใช้ขอบเขตส่วนประกอบสถาปัตยกรรม


1
คุณมีข้อผิดพลาดในการคอมไพล์@Query abstract suspend fun count()โดยใช้คำหลัก Suspend หรือไม่ คุณช่วยดูคำถามที่คล้ายกันนี้ได้ไหมstackoverflow.com/questions/48694449/…
โรบิน

@ โรบิน - ใช่ฉันทำ ความผิดพลาดของฉัน; ฉันกำลังใช้วิธีการระงับบน DAO สาธารณะ (ไม่มีคำอธิบาย) ที่เรียกว่า@Queryฟังก์ชันnon-suspend ที่มีการป้องกัน เมื่อฉันเพิ่มคีย์เวิร์ด Suspend ใน@Queryเมธอดภายในเกินไปมันก็ไม่สามารถคอมไพล์ได้ ดูเหมือนว่าคนฉลาดที่อยู่ภายใต้ประทุนสำหรับการปะทะกันของ Suspend & Room (ตามที่คุณพูดถึงในคำถามอื่นของคุณการระงับเวอร์ชันที่รวบรวมแล้วกำลังส่งคืนความต่อเนื่องซึ่ง Room ไม่สามารถจัดการได้)
AjahnCharles

เข้าท่ามาก. ผมจะเรียกมันด้วยฟังก์ชันโครูทีนแทน
Robin

1
@Robin - FYI พวกเขาเพิ่มการสนับสนุนสำหรับการระงับในห้อง 2.1 :)
AjahnCharles

เห็นได้ชัดว่าไม่มีlaunchคีย์เวิร์ดอีกต่อไปคุณเปิดตัวด้วยขอบเขตเช่นGlobalScope.launch
nasch

48

สำหรับทุกRxJavaหรือRxAndroidหรือRxKotlinคนรักออกมี

Observable.just(db)
          .subscribeOn(Schedulers.io())
          .subscribe { db -> // database operation }

3
หากฉันใส่รหัสนี้ในเมธอดจะส่งคืนผลลัพธ์จากการทำงานของฐานข้อมูลได้อย่างไร
Eggakin Baconwalker

@EggakinBaconwalker ฉันมีoverride fun getTopScores(): Observable<List<PlayerScore>> { return Observable .fromCallable({ GameApplication.database .playerScoresDao().getTopScores() }) .applySchedulers() }ที่ไหนapplySchedulers()ก็ทำfun <T> Observable<T>.applySchedulers(): Observable<T> = this.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())
noloman

สิ่งนี้ใช้ไม่ได้กับ IntentService เนื่องจาก IntentService จะทำเมื่อเธรดเสร็จสมบูรณ์
Umang Kothari

1
@UmangKothari คุณจะไม่ได้รับข้อยกเว้นหากคุณเปิดอยู่IntentService#onHandleIntentเนื่องจากวิธีนี้รันบนเธรดของผู้ปฏิบัติงานดังนั้นคุณจึงไม่จำเป็นต้องมีกลไกเธรดใด ๆ ในการดำเนินการฐานข้อมูลห้อง
ซามูเอลโรเบิร์ต

@SamuelRobert ใช่ยอมรับความเลวของฉัน มันทำให้จิตใจของฉันลื่น
Umang Kothari

27

คุณไม่สามารถรันบนเธรดหลักแทนใช้ตัวจัดการ async หรือเธรดที่ใช้งานได้ โค้ดตัวอย่างมีอยู่ที่นี่และอ่านบทความในไลบรารีห้องได้ที่นี่: Room Library ของ 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()


ถ้าฉันใช้วิธีนี้เพื่อรับข้อมูล (เฉพาะ getAllUsers () ในกรณีนี้) จะส่งคืนข้อมูลจากวิธีนี้ได้อย่างไร ปรากฏข้อผิดพลาดถ้าฉันใส่คำว่า "return" ไว้ใน "run"
Eggakin Baconwalker

1
สร้างวิธีการเชื่อมต่อที่ใดที่หนึ่งและเพิ่มคลาสที่ไม่ระบุชื่อเพื่อรับข้อมูลจากที่นี่
Rizvan

1
นี่เป็นวิธีที่ง่ายที่สุดสำหรับการแทรก / อัปเดต
Beer Me

12

ด้วยแลมบ์ดาจึงทำงานได้ง่ายด้วย AsyncTask

 AsyncTask.execute(() -> //run your query here );

2
สะดวกขอบคุณ ยังไงก็ตาม Kotlin นั้นง่ายยิ่งกว่าเดิม: AsyncTask.execute {}
alexrnov

1
แต่คุณจะได้ผลลัพธ์โดยใช้วิธีนี้อย่างไร?
leeCoder

11

ด้วยไลบรารี Jetbrains Anko คุณสามารถใช้เมธอด doAsync {.. } เพื่อเรียกใช้ฐานข้อมูลโดยอัตโนมัติ สิ่งนี้จะดูแลปัญหาการใช้คำฟุ่มเฟือยที่คุณดูเหมือนจะได้รับจากคำตอบของ mcastro

ตัวอย่างการใช้งาน:

    doAsync { 
        Application.database.myDAO().insertUser(user) 
    }

ฉันใช้สิ่งนี้บ่อยครั้งสำหรับการแทรกและการอัปเดตอย่างไรก็ตามสำหรับแบบสอบถามที่เลือกฉันแนะนำใหม่โดยใช้เวิร์กโฟลว์ RX



6

คุณต้องดำเนินการตามคำขอในพื้นหลัง วิธีง่ายๆอาจใช้Executors :

Executors.newSingleThreadExecutor().execute { 
   yourDb.yourDao.yourRequest() //Replace this by your request
}

คุณส่งผลลัพธ์อย่างไร
leeCoder

5

มีการใช้โซลูชัน RxJava / Kotlin ที่สวยงามCompletable.fromCallableซึ่งจะให้ Observable แก่คุณซึ่งไม่ส่งคืนค่า แต่สามารถสังเกตและสมัครสมาชิกในเธรดอื่นได้

public Completable insert(Event event) {
    return Completable.fromCallable(new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            return database.eventDao().insert(event)
        }
    }
}

หรือใน Kotlin:

fun insert(event: Event) : Completable = Completable.fromCallable {
    database.eventDao().insert(event)
}

คุณสามารถสังเกตและสมัครสมาชิกได้ตามปกติ:

dataManager.insert(event)
    .subscribeOn(scheduler)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...)

5

คุณสามารถอนุญาตการเข้าถึงฐานข้อมูลบนเธรดหลัก แต่เพื่อวัตถุประสงค์ในการดีบักเท่านั้นคุณไม่ควรทำเช่นนี้ในการผลิต

นี่คือเหตุผล

หมายเหตุ: Room ไม่รองรับการเข้าถึงฐานข้อมูลบนเธรดหลักเว้นแต่คุณจะเรียก allowMainThreadQueries () บนตัวสร้างเนื่องจากอาจล็อก UI เป็นเวลานาน การสืบค้นแบบอะซิงโครนัส - แบบสอบถามที่ส่งคืนอินสแตนซ์ของ LiveData หรือ Flowable - จะได้รับการยกเว้นจากกฎนี้เนื่องจากจะเรียกใช้แบบสอบถามบนเธรดพื้นหลังแบบอะซิงโครนัสเมื่อจำเป็น


4

คุณสามารถใช้รหัสนี้เพื่อแก้ปัญหาได้:

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()ด้วยรหัสของคุณเอง


4

เนื่องจาก asyncTask เลิกใช้แล้วเราอาจใช้บริการตัวดำเนินการ หรือคุณยังสามารถใช้ ViewModel กับLiveDataตามที่อธิบายไว้ในคำตอบอื่น ๆ

สำหรับการใช้บริการตัวดำเนินการคุณสามารถใช้สิ่งต่อไปนี้

public class DbHelper {

    private final Executor executor = Executors.newSingleThreadExecutor();

    public void fetchData(DataFetchListener dataListener){
        executor.execute(() -> {
                Object object = retrieveAgent(agentId);
                new Handler(Looper.getMainLooper()).post(() -> {
                        dataListener.onFetchDataSuccess(object);
                });
        });
    }
}

ใช้ Main Looper เพื่อให้คุณสามารถเข้าถึงองค์ประกอบ UI จากการonFetchDataSuccessโทรกลับ


3

ข้อความแสดงข้อผิดพลาด

ไม่สามารถเข้าถึงฐานข้อมูลบนเธรดหลักเนื่องจากอาจล็อก UI เป็นเวลานาน

ค่อนข้างอธิบายและถูกต้อง คำถามคือคุณควรหลีกเลี่ยงการเข้าถึงฐานข้อมูลบนเธรดหลักได้อย่างไร นั่นเป็นหัวข้อใหญ่ แต่ในการเริ่มต้นอ่านเกี่ยวกับAsyncTask (คลิกที่นี่)

----- แก้ไข ----------

ฉันเห็นว่าคุณกำลังมีปัญหาเมื่อเรียกใช้การทดสอบหน่วย คุณมีสองทางเลือกในการแก้ไขปัญหานี้:

  1. ทำการทดสอบโดยตรงบนเครื่องพัฒนาแทนบนอุปกรณ์ Android (หรือโปรแกรมจำลอง) วิธีนี้ใช้ได้กับการทดสอบที่เน้นฐานข้อมูลเป็นศูนย์กลางและไม่สนใจว่าจะทำงานบนอุปกรณ์หรือไม่

  2. ใช้คำอธิบายประกอบ @RunWith(AndroidJUnit4.class) เพื่อเรียกใช้การทดสอบบนอุปกรณ์ Android แต่ไม่ใช่ในกิจกรรมที่มี UI สามารถดูรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ในบทช่วยสอนนี้


ฉันเข้าใจประเด็นของคุณสมมติฐานของฉันคือจุดเดียวกันนี้ใช้ได้เมื่อคุณพยายามทดสอบการทำงานของฐานข้อมูลใด ๆ ผ่าน JUnit อย่างไรก็ตามในdeveloper.android.com/topic/libraries/architecture/room.htmlวิธีการทดสอบตัวอย่าง writeUserAndReadInList ไม่เรียกใช้แบบสอบถามแทรกบนเธรดพื้นหลัง ฉันพลาดอะไรตรงนี้ไปหรือเปล่า? ช่วยแนะนำหน่อยครับ.
Devarshi

ขออภัยฉันพลาดข้อเท็จจริงที่ว่านี่เป็นการทดสอบที่มีปัญหา ฉันจะแก้ไขคำตอบเพื่อเพิ่มข้อมูลเพิ่มเติม
Dale Wilson

3

หากคุณพอใจกับงาน Asyncมากขึ้น:

  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();

2

อัปเดต: ฉันได้รับข้อความนี้เมื่อพยายามสร้างแบบสอบถามโดยใช้ @RawQuery และ SupportSQLiteQuery ภายใน DAO

@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


1

สำหรับการสืบค้นด่วนคุณสามารถอนุญาตให้มีห้องเพื่อดำเนินการบนเธรด 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);
        }
    }

1

คุณสามารถใช้ 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();
}

นี่คือเหตุผลที่เราใช้ Callable / Future เนื่องจาก Android ไม่อนุญาตให้เรียกใช้แบบสอบถามบนเธรดหลัก ตามที่ถามใน qus ข้างต้น
ผู้เริ่มต้น

1
ฉันหมายความว่าแม้ว่ารหัสจากคำตอบของคุณจะสร้างข้อความค้นหาในเธรดพื้นหลัง แต่เธรดหลักจะถูกบล็อกและรอเมื่อการสืบค้นเสร็จสิ้น allowMainThreadQueries()ดังนั้นในท้ายที่สุดมันก็ไม่ได้ดีกว่า เธรดหลักยังคงถูกบล็อกในทั้งสองกรณี
eugeneek

0

ในความคิดของฉันสิ่งที่ถูกต้องที่ต้องทำคือมอบหมายแบบสอบถามให้กับเธรด IO โดยใช้ RxJava

ฉันมีตัวอย่างวิธีแก้ปัญหาเทียบเท่าที่ฉันเพิ่งเจอ

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            //Creating view model requires DB access
            homeViewModel = new ViewModelProvider(this, factory).get(HomeViewModel.class);
        }).subscribeOn(Schedulers.io())//The DB access executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    mAdapter = new MyAdapter(homeViewModel.getExams());
                    recyclerView.setAdapter(mAdapter);
                    ((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.INVISIBLE);
                },
                error -> error.printStackTrace()
        );

และถ้าเราต้องการสรุปวิธีแก้ปัญหา:

((ProgressBar) view.findViewById(R.id.progressBar_home)).setVisibility(View.VISIBLE);//Always good to set some good feedback
        Completable.fromAction(() -> {
            someTaskThatTakesTooMuchTime();
        }).subscribeOn(Schedulers.io())//The long task executes on a non-main-thread thread
        .observeOn(AndroidSchedulers.mainThread())//Upon completion of the DB-involved execution, the continuation runs on the main thread
        .subscribe(
                () ->
                {
                    taskIWantToDoOnTheMainThreadWhenTheLongTaskIsDone();
                },
                error -> error.printStackTrace()
        );
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.