แนวปฏิบัติที่ดีที่สุดสำหรับ SQLite บน Android คืออะไร


694

สิ่งที่จะได้รับการพิจารณาว่าเป็นแนวปฏิบัติที่ดีที่สุดเมื่อเรียกใช้งานแบบสอบถามบนฐานข้อมูล SQLite ภายในแอป Android

การเรียกใช้แทรกลบและเลือกคิวรีจาก doInBackground ของ AsyncTask ปลอดภัยหรือไม่? หรือฉันควรใช้เธรด UI ฉันคิดว่าการสืบค้นฐานข้อมูลอาจ "หนัก" และไม่ควรใช้เธรด UI เนื่องจากสามารถล็อกแอพได้ - ทำให้แอปพลิเคชันไม่ตอบสนอง (ANR)

หากฉันมีหลาย AsyncTasks พวกเขาควรแบ่งปันการเชื่อมต่อหรือพวกเขาควรเปิดการเชื่อมต่อแต่ละคน?

มีแนวทางปฏิบัติที่ดีที่สุดสำหรับสถานการณ์เหล่านี้หรือไม่?


10
ไม่ว่าคุณจะทำอะไรอย่าลืมทำความสะอาดอินพุตของคุณถ้าผู้ให้บริการเนื้อหา (หรือส่วนต่อประสาน SQLite) ของคุณปรากฏต่อสาธารณะ!
Kristopher Micinski

37
แน่นอนคุณไม่ควรทำการเข้าถึงฐานข้อมูลจากเธรด UI ฉันสามารถบอกคุณได้มาก
Edward Falk

@EdwardFalk ทำไมล่ะ? มีกรณีการใช้งานที่ถูกต้องหรือไม่
Michael

4
หากคุณทำ I / O เครือข่ายเข้าถึง ฯลฯ จากเธรด UI อุปกรณ์ทั้งหมดจะค้างจนกว่าการดำเนินการจะเสร็จสิ้น ถ้ามันเสร็จใน 1/20 วินาทีก็โอเค หากใช้เวลานานขึ้นคุณจะได้รับประสบการณ์การใช้งานที่ไม่ดี
Edward Falk

คำตอบ:


631

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

คำตอบพื้นฐาน

วัตถุ SqliteOpenHelper มีการเชื่อมต่อกับฐานข้อมูลเดียว ดูเหมือนว่าจะให้การเชื่อมต่อแบบอ่านและเขียนให้คุณ แต่มันไม่จริง โทรหาแบบอ่านอย่างเดียวและคุณจะได้รับการเชื่อมต่อฐานข้อมูลการเขียนไม่ว่า

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

ดังนั้นหนึ่งผู้ช่วยหนึ่งการเชื่อมต่อฐานข้อมูลซึ่งเป็นอนุกรมในรหัสจาวา หนึ่งเธรด 1,000 เธรดถ้าคุณใช้หนึ่งอินสแตนซ์ตัวช่วยเหลือที่แบ่งใช้ระหว่างกันรหัสการเข้าถึง db ทั้งหมดของคุณเป็นแบบอนุกรม และชีวิตก็ดี (ish)

หากคุณพยายามที่จะเขียนไปยังฐานข้อมูลจากการเชื่อมต่อที่แตกต่างกันจริงในเวลาเดียวกันหนึ่งจะล้มเหลว มันจะไม่รอจนกว่าจะเสร็จก่อนแล้วจึงเขียน มันจะไม่เขียนการเปลี่ยนแปลงของคุณ ที่แย่กว่านั้นถ้าคุณไม่ได้เรียกรุ่นของการแทรก / อัพเดทที่ถูกต้องบน SQLiteDatabase คุณจะไม่ได้รับข้อยกเว้น คุณจะได้รับข้อความใน LogCat ของคุณและมันจะเป็นเช่นนั้น

ดังนั้นหลายกระทู้ ใช้ผู้ช่วยหนึ่งคน ระยะเวลา หากคุณรู้ว่ามีเพียงหนึ่งเธรดที่กำลังเขียนคุณอาจสามารถใช้การเชื่อมต่อได้หลายรายการและการอ่านของคุณจะเร็วขึ้น แต่ผู้ซื้อระวัง ฉันไม่ได้ทดสอบมากขนาดนั้น

นี่คือโพสต์บล็อกที่มีรายละเอียดเพิ่มเติมและแอปตัวอย่าง

เกรย์และฉันกำลังห่อเครื่องมือ ORM ตาม Ormlite ของเขาซึ่งทำงานได้ดีกับการใช้ฐานข้อมูล Android และตามโครงสร้างการสร้าง / การโทรที่ปลอดภัยที่ฉันอธิบายไว้ในบล็อกโพสต์ ที่ควรจะออกเร็ว ๆ นี้ ลองดูสิ.


ในระหว่างนั้นมีโพสต์บล็อกติดตามอยู่:

เช็คเอาต์ fork จาก2point0ของตัวอย่างการล็อกที่กล่าวถึงก่อนหน้าด้วย:


2
เช่นกันการสนับสนุนหุ่นยนต์ Ormlite สามารถพบได้ที่ormlite.sourceforge.net/sqlite_java_android_orm.html มีตัวอย่างโครงการเอกสารและขวด
สีเทา

1
ที่สองกัน รหัส ormlite มีคลาสตัวช่วยที่สามารถใช้เพื่อจัดการอินสแตนซ์ dbhelper คุณสามารถใช้สิ่ง ormlite แต่ไม่จำเป็นต้องใช้ คุณสามารถใช้คลาสตัวช่วยเพื่อทำการจัดการการเชื่อมต่อ
Kevin Galligan

31
สวัสดี, ขอบคุณสำหรับคำอธิบายโดยละเอียด คุณช่วยอธิบายสิ่งหนึ่งได้ไหม - ฉันเข้าใจว่าคุณควรมีผู้ช่วยหนึ่งคน แต่คุณควรมีเพียงหนึ่งการเชื่อมต่อ (เช่นหนึ่งวัตถุของ SqliteDatabase) คุณควรโทรหาฐานข้อมูล getWritableDatabase บ่อยแค่ไหน? และที่สำคัญไม่แพ้กันเมื่อคุณโทรใกล้ ()?
Artem

3
ฉันอัปเดตรหัสแล้ว ต้นฉบับหายไปเมื่อฉันเปลี่ยนโฮสต์ของบล็อก แต่ฉันเพิ่มโค้ดตัวอย่างบางลงซึ่งจะแสดงให้เห็นถึงปัญหา นอกจากนี้คุณจะจัดการการเชื่อมต่อเดียวได้อย่างไร ตอนแรกฉันมีวิธีแก้ปัญหาที่ซับซ้อนกว่านี้มาก แต่ฉันก็แก้ไขมันตั้งแต่นั้นมา ลองดูที่นี่: touchlab.co/uncategorized/single-sqlite-connection
Kevin Galligan

จำเป็นต้องกำจัดการเชื่อมต่อหรือไม่และจะทำอย่างไร
tugce

189

การเข้าถึงฐานข้อมูลพร้อมกัน

บทความเดียวกันในบล็อกของฉัน (ฉันชอบการจัดรูปแบบเพิ่มเติม)

ฉันเขียนบทความเล็ก ๆ ซึ่งอธิบายวิธีการเข้าถึงเธรดฐานข้อมูล android ของคุณอย่างปลอดภัย


สมมติว่าคุณมีของคุณเองSQLiteOpenHelper

public class DatabaseHelper extends SQLiteOpenHelper { ... }

ตอนนี้คุณต้องการเขียนข้อมูลไปยังฐานข้อมูลในหัวข้อแยก

 // Thread 1
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

 // Thread 2
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

คุณจะได้รับข้อความต่อไปนี้ใน logcat ของคุณและหนึ่งในการเปลี่ยนแปลงของคุณจะไม่ถูกเขียน

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

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

ในการใช้ฐานข้อมูลที่มีหลายเธรดเราต้องแน่ใจว่าเราใช้การเชื่อมต่อฐานข้อมูลเดียว

ลองสร้างตัวจัดการฐานข้อมูลแบบชั้นเดียวซึ่งจะเก็บและส่งคืนวัตถุSQLiteOpenHelperเดียว

public class DatabaseManager {

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initialize(..) method first.");
        }

        return instance;
    }

    public SQLiteDatabase getDatabase() {
        return new mDatabaseHelper.getWritableDatabase();
    }

}

อัปเดตรหัสซึ่งเขียนข้อมูลไปยังฐานข้อมูลในกระทู้แยกจะมีลักษณะเช่นนี้

 // In your application class
 DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
 // Thread 1
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

 // Thread 2
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

นี่จะทำให้คุณพังอีก

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

เนื่องจากเรามีการใช้เพียงการเชื่อมต่อฐานข้อมูลหนึ่งวิธีgetDatabase ()กลับเช่นเดียวกันของSQLiteDatabaseวัตถุสำหรับThread1และThread2 เกิดอะไรขึ้นThread1อาจปิดฐานข้อมูลในขณะที่Thread2ยังคงใช้งานอยู่ นั่นเป็นสาเหตุที่เรามีIllegalStateExceptionขัดข้อง

เราต้องตรวจสอบให้แน่ใจว่าไม่มีใครใช้ฐานข้อมูลและปิดแล้วเท่านั้น บางคนใน stackoveflow แนะนำให้ไม่เคยใกล้ชิดของคุณSQLiteDatabase ซึ่งจะส่งผลให้ข้อความ logcat ดังต่อไปนี้

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

ตัวอย่างการทำงาน

public class DatabaseManager {

    private int mOpenCounter;

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mDatabase;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) method first.");
        }

        return instance;
    }

    public synchronized SQLiteDatabase openDatabase() {
        mOpenCounter++;
        if(mOpenCounter == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

    public synchronized void closeDatabase() {
        mOpenCounter--;
        if(mOpenCounter == 0) {
            // Closing database
            mDatabase.close();

        }
    }

}

ใช้มันดังต่อไปนี้

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way

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

เช่นเดียวกับที่เกิดขึ้นในcloseDatabase ()วิธีการ ทุกครั้งที่เราเรียกเมธอดนี้ตัวนับจะลดลงเมื่อใดก็ตามที่เป็นศูนย์เราจะปิดการเชื่อมต่อฐานข้อมูล


ตอนนี้คุณควรจะสามารถใช้ฐานข้อมูลของคุณและตรวจสอบให้แน่ใจว่ามันปลอดภัย


10
ฉันเป็นหนึ่งในคนเหล่านั้นที่แนะนำให้คุณไม่เคยปิดฐานข้อมูล คุณจะได้รับข้อผิดพลาด "พบรอยรั่ว" หากคุณเปิดฐานข้อมูลอย่าปิดและลองเปิดใหม่อีกครั้ง หากคุณใช้ตัวช่วยเปิดเดียวและไม่ปิดฐานข้อมูลคุณจะไม่ได้รับข้อผิดพลาดนั้น หากคุณพบเป็นอย่างอื่นโปรดแจ้งให้เราทราบ (พร้อมรหัส) ฉันมีโพสต์อีกต่อไปเกี่ยวกับมันที่ไหนสักแห่ง แต่ไม่สามารถหาได้ ตอบคำถามโดยคอมมอนส์แวร์ที่นี่ใครช่วยเราทั้งสองในแผนกคะแนน SO: stackoverflow.com/questions/7211941/…
Kevin Galligan

4
ความคิดเพิ่มเติม # 1 ฉันจะสร้างผู้ช่วยภายในผู้จัดการของคุณ ขอให้มีปัญหากับมันข้างนอก ผู้พัฒนารายใหม่อาจเรียกผู้ช่วยโดยตรงด้วยเหตุผลบางอย่างที่บ้าคลั่ง นอกจากนี้หากคุณต้องการวิธีการเริ่มต้นให้ยกเว้นถ้ามีอินสแตนซ์อยู่แล้ว แอพหลายฐานข้อมูลจะล้มเหลวอย่างเห็นได้ชัด # 2 ทำไมฟิลด์ mDatabase มันมาจากผู้ช่วย # 3 ในฐานะก้าวแรกของคุณที่จะ "ไม่ปิด" จะเกิดอะไรขึ้นกับ db ของคุณเมื่อแอปของคุณขัดข้องและไม่ได้ "ปิด" คำแนะนำไม่มีอะไร มันดีเพราะ SQLite นั้นเสถียรมาก นั่นคือขั้นตอนที่ 1 ในการหาสาเหตุที่คุณไม่จำเป็นต้องปิดมัน
Kevin Galligan

1
เหตุผลใดที่คุณใช้วิธีการเริ่มต้นสาธารณะเพื่อโทรก่อนรับอินสแตนซ์? ทำไมไม่ได้นวกรรมิกเอกชนที่เรียกว่าif(instance==null)? คุณไม่มีทางเลือกนอกจากโทรไปเริ่มต้นใหม่ทุกครั้ง คุณจะรู้ได้อย่างไรว่ามันได้รับการเริ่มต้นหรือไม่ในแอปพลิเคชันอื่น ๆ ?
ChiefTwoPencils

1
initializeInstance()มีพารามิเตอร์ของชนิดแต่ในความคิดเห็นของคุณที่คุณกล่าวถึงการใช้SQLiteOpenHelper DatabaseManager.initializeInstance(getApplicationContext());เกิดอะไรขึ้น? สิ่งนี้สามารถทำงานได้อย่างไร
faizal

2
@DmytroDanylyk "DatabaseManager เป็นเธรดเดี่ยวที่ปลอดภัยดังนั้นจึงสามารถใช้ได้ทุกที่" ซึ่งไม่เป็นความจริงในการตอบคำถามของ virsir วัตถุไม่ได้ใช้กระบวนการข้ามที่ใช้ร่วมกัน DatabaseManager ของคุณจะไม่มีสถานะในกระบวนการที่แตกต่างเช่นในอะแดปเตอร์ซิงค์ (android: process = ": sync")
whizzle

17
  • ใช้ThreadหรือAsyncTaskสำหรับการทำงานที่ยาวนาน (50ms +) ทดสอบแอปของคุณเพื่อดูว่ามันอยู่ที่ไหน การดำเนินการส่วนใหญ่ (อาจ) ไม่ต้องการเธรดเนื่องจากการดำเนินการส่วนใหญ่ (อาจ) เกี่ยวข้องกับแถวเพียงไม่กี่แถว ใช้เธรดสำหรับการทำงานแบบกลุ่ม
  • แบ่งปันหนึ่งSQLiteDatabaseอินสแตนซ์สำหรับแต่ละ DB บนดิสก์ระหว่างเธรดและใช้ระบบการนับเพื่อติดตามการเชื่อมต่อที่เปิดอยู่

มีแนวทางปฏิบัติที่ดีที่สุดสำหรับสถานการณ์เหล่านี้หรือไม่?

แชร์ฟิลด์สแตติกระหว่างคลาสทั้งหมดของคุณ ฉันเคยเก็บรอบเดียวสำหรับสิ่งนั้นและสิ่งอื่น ๆ ที่ต้องใช้ร่วมกัน ควรใช้แบบแผนการนับ (โดยทั่วไปใช้ AtomicInteger) เพื่อให้แน่ใจว่าคุณไม่เคยปิดฐานข้อมูลก่อนหรือเปิดทิ้งไว้

ทางออกของฉัน:

สำหรับเวอร์ชั่นล่าสุดโปรดดูhttps://github.com/JakarCo/databasemanagerแต่ฉันจะพยายามทำให้รหัสเป็นปัจจุบันอยู่ที่นี่เช่นกัน หากคุณต้องการที่จะเข้าใจวิธีการแก้ปัญหาของฉันดูที่รหัสและอ่านบันทึกของฉัน บันทึกของฉันมักจะมีประโยชน์ทีเดียว

  1. คัดลอก / DatabaseManagerวางรหัสลงในไฟล์ใหม่ที่ชื่อว่า (หรือดาวน์โหลดจาก GitHub)
  2. ขยายDatabaseManagerและใช้งานonCreateและonUpgradeตามปกติ คุณสามารถสร้างคลาสย่อยหลายคลาสของหนึ่งDatabaseManagerคลาสเพื่อให้มีฐานข้อมูลต่าง ๆ บนดิสก์
  3. ยกตัวอย่างคลาสย่อยของคุณและเรียกgetDb()ใช้SQLiteDatabaseคลาส
  4. เรียกclose()แต่ละคลาสย่อยที่คุณสร้างอินสแตนซ์

รหัสที่จะคัดลอก / วาง :

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at support@androidsqlitelibrary.com (or support@jakar.co )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onUpgrade(db, oldVersion, newVersion);
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            databaseManager.onOpen(db);
        }

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}

1
จะเกิดอะไรขึ้นเมื่อคุณเรียกว่า "ปิด" จากนั้นลองใช้คลาสอีกครั้ง มันจะผิดพลาดหรือไม่ หรือจะเริ่มต้นใหม่เองโดยอัตโนมัติเพื่อให้สามารถใช้ฐานข้อมูลอีกครั้งได้หรือไม่
นักพัฒนาซอฟต์แวร์ Android

1
@androiddeveloper, ถ้าคุณโทรclose, คุณต้องโทรopenอีกครั้งก่อนที่จะใช้อินสแตนซ์เดียวกันของคลาสหรือคุณสามารถสร้างอินสแตนซ์ใหม่ได้ เนื่องจากในcloseรหัสฉันตั้งค่าdb=nullคุณจะไม่สามารถใช้ค่าส่งคืนจากgetDb(เนื่องจากมันจะเป็นโมฆะ) ดังนั้นคุณจะได้รับNullPointerExceptionหากคุณทำสิ่งที่ต้องการmyInstance.close(); myInstance.getDb().query(...);
Reed

ทำไมไม่รวมgetDb()และopen()เป็นวิธีการเดียว?
Alex Burdusel

@Burdu เป็นการผสมผสานระหว่างการควบคุมเคาน์เตอร์ฐานข้อมูลและการออกแบบที่ไม่ดีเป็นพิเศษ มันไม่ใช่วิธีที่ดีที่สุดที่จะทำ ฉันจะอัปเดตในอีกไม่กี่วัน
รีด

@Burdu ฉันเพิ่งอัปเดต คุณจะได้รับรหัสใหม่จากที่นี่ ฉันยังไม่ได้ทดสอบดังนั้นโปรดแจ้งให้ฉันทราบหากฉันควรจะยอมรับการเปลี่ยนแปลง
ลิ้น

11

ฐานข้อมูลมีความยืดหยุ่นมากในการใช้เธรดหลายเธรด แอพของฉันไปที่ฐานข้อมูลของตนเองจากหลาย ๆ เธรดพร้อมกันและทำได้ดี ในบางกรณีฉันมีหลายกระบวนการที่กดปุ่มฐานข้อมูลพร้อมกันและทำงานได้ดีเช่นกัน

งาน async ของคุณ - ใช้การเชื่อมต่อเดียวกันเมื่อคุณสามารถทำได้ แต่ถ้าคุณต้องการมันก็โอเคที่จะเข้าถึงฐานข้อมูลจากงานที่แตกต่างกัน


นอกจากนี้คุณมีผู้อ่านและนักเขียนในการเชื่อมต่อที่แตกต่างกันหรือพวกเขาควรแบ่งปันการเชื่อมต่อเดียว? ขอบคุณ
สีเทา

@Gray - ถูกต้องฉันควรจะกล่าวถึงอย่างชัดเจนว่า เท่าที่การเชื่อมต่อฉันจะใช้การเชื่อมต่อเดียวกันให้มากที่สุด แต่เนื่องจากการล็อคถูกจัดการที่ระดับระบบแฟ้มคุณสามารถเปิดได้หลายครั้งในรหัส แต่ฉันจะใช้การเชื่อมต่อเดียวให้มากที่สุด SQL sqlite DB นั้นยืดหยุ่นและให้อภัย
แบรดไฮน์

3
@Gray เพียงต้องการโพสต์ข้อมูลที่อัปเดตสำหรับผู้ที่อาจใช้วิธีนี้ เอกสารอธิบายว่า: วิธีนี้ไม่ทำอะไรเลย ไม่ได้ใช้.
Pijusn

3
ฉันพบวิธีนี้เพื่อล้มเหลวอย่างน่ากลัวเรากำลังสลับไปที่ ContentProvider เพื่อเข้าถึงจากหลายแอปพลิเคชัน เราจะต้องทำพร้อมกันในวิธีการของเรา แต่ที่ควรแก้ไขปัญหาใด ๆ กับกระบวนการทั้งหมดเข้าถึงข้อมูลในเวลาเดียวกัน
JPM

2
ฉันรู้ว่านี่เก่า แต่มันไม่ถูกต้อง มันอาจใช้งานได้ดีในการเข้าถึงฐานข้อมูลจากSQLiteDatabaseวัตถุต่างๆ ในAsyncTasks / s ที่แตกต่างกันThreadแต่บางครั้งจะนำไปสู่ข้อผิดพลาดซึ่งเป็นสาเหตุที่SQLiteDatabase (บรรทัด 1297) ใช้Locks
Reed

7

คำตอบของ Dmytro ทำงานได้ดีสำหรับกรณีของฉัน ฉันคิดว่าเป็นการดีกว่าที่จะประกาศฟังก์ชั่นการซิงโครไนซ์ อย่างน้อยสำหรับกรณีของฉันมันจะเรียกใช้ข้อยกเว้นตัวชี้โมฆะตัวอย่างเช่น getWritableDatabase ยังไม่ได้ส่งคืนในเธรดหนึ่งและ openDatabse ที่เรียกในเธรดอื่นในขณะเดียวกัน

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}

mDatabaseHelper.getWritableDatabase (); สิ่งนี้จะไม่สร้างออบเจ็กต์ฐานข้อมูลใหม่
นอก

5

หลังจากดิ้นรนกับสิ่งนี้เป็นเวลาสองสามชั่วโมงฉันพบว่าคุณสามารถใช้หนึ่งวัตถุตัวช่วย db ต่อการประมวลผล db ตัวอย่างเช่น,

for(int x = 0; x < someMaxValue; x++)
{
    db = new DBAdapter(this);
    try
    {

        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }
    db.close();
}

ดังที่ได้แสดงต่อไปนี้:

db = new DBAdapter(this);
for(int x = 0; x < someMaxValue; x++)
{

    try
    {
        // ask the database manager to add a row given the two strings
        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }

}
db.close();

การสร้าง DBAdapter ใหม่ทุกครั้งที่วนซ้ำเป็นวิธีเดียวที่ฉันสามารถดึงสตริงของฉันลงในฐานข้อมูลผ่านคลาสผู้ช่วยของฉัน


4

ความเข้าใจของฉันเกี่ยวกับ SQLiteDatabase APIs คือในกรณีที่คุณมีแอพพลิเคชั่นแบบมัลติเธรดคุณไม่สามารถที่จะมีวัตถุ SQLiteDatabase มากกว่า 1 รายการที่ชี้ไปยังฐานข้อมูลเดียว

สามารถสร้างวัตถุได้อย่างแน่นอน แต่การแทรก / การปรับปรุงล้มเหลวหากเธรด / กระบวนการที่แตกต่างกัน (เช่นกัน) เริ่มใช้วัตถุ SQLiteDatabase ที่แตกต่างกัน (เช่นวิธีที่เราใช้ในการเชื่อมต่อ JDBC)

ทางออกเดียวที่นี่คือการยึดติดกับวัตถุ 1 SQLiteDatabase และเมื่อใดก็ตามที่มีการใช้ startTransaction () ในมากกว่า 1 เธรด Android จะจัดการการล็อกข้ามเธรดที่แตกต่างกันและอนุญาตให้ 1 เธรดในเวลาเดียว

นอกจากนี้คุณสามารถทำ "อ่าน" จากฐานข้อมูลและใช้วัตถุ SQLiteDatabase เดียวกันในเธรดอื่น (ในขณะที่เธรดอื่นเขียน) และจะไม่มีความเสียหายของฐานข้อมูลนั่นคือ "read thread" จะไม่อ่านข้อมูลจากฐานข้อมูลจนถึง " เขียนเธรด "ส่งข้อมูลแม้ว่าทั้งคู่จะใช้วัตถุ SQLiteDatabase เดียวกัน

ซึ่งแตกต่างจากวิธีการเชื่อมต่อวัตถุใน JDBC ที่ถ้าคุณผ่าน (ใช้เหมือนกัน) วัตถุการเชื่อมต่อระหว่างอ่านและเขียนหัวข้อแล้วเราน่าจะพิมพ์ข้อมูลปราศจากข้อผูกมัดเกินไป

ในแอปพลิเคชันองค์กรของฉันฉันพยายามใช้การตรวจสอบแบบมีเงื่อนไขเพื่อให้เธรด UI ไม่ต้องรอในขณะที่เธรด BG มีวัตถุ SQLiteDatabase (เฉพาะ) ฉันพยายามทำนายการกระทำของ UI และเลื่อนเธรด BG จากการทำงานเป็นเวลา 'x' วินาที นอกจากนี้เรายังสามารถดูแล PriorityQueue เพื่อจัดการการแจกออบเจ็กต์การเชื่อมต่อ SQLiteDatabase เพื่อให้เธรด UI ได้รับก่อน


และสิ่งใดที่คุณใส่ลงใน PriorityQueue - Listeners (ที่ต้องการรับวัตถุฐานข้อมูล) หรือแบบสอบถาม SQL
Pijusn

ฉันไม่ได้ใช้วิธีการจัดลำดับความสำคัญ แต่เป็นหัวข้อ "ผู้โทร"
Swaroop

@Swaroop: "read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase objectPCMIIW, สิ่งนี้ไม่เป็นความจริงเสมอไปหากคุณเริ่มต้น "read thread" หลังจาก "write thread" คุณอาจไม่ได้รับข้อมูลที่เพิ่งปรับปรุง (แทรกหรืออัพเดทใน write thread) อ่านเธรดอาจอ่านข้อมูลก่อนเริ่มเขียนเธรด สิ่งนี้เกิดขึ้นเนื่องจากการดำเนินการเขียนเปิดใช้งานการล็อกสำรองแทนการล็อคแบบเอกสิทธิ์
Amit Vikram Singh

4

คุณสามารถพยายามที่จะใช้วิธีการสถาปัตยกรรมใหม่anouncedที่ Google I / O 2017

นอกจากนี้ยังมีไลบรารี ORM ใหม่ที่ชื่อว่าRoom

มันมีสามองค์ประกอบหลัก: @Entity, @Dao และ @Database

User.java

@Entity
public class User {
  @PrimaryKey
  private int uid;

  @ColumnInfo(name = "first_name")
  private String firstName;

  @ColumnInfo(name = "last_name")
  private String lastName;

  // Getters and setters are ignored for brevity,
  // but they're required for Room to work.
}

UserDao.java

@Dao
public interface UserDao {
  @Query("SELECT * FROM user")
  List<User> getAll();

  @Query("SELECT * FROM user WHERE uid IN (:userIds)")
  List<User> loadAllByIds(int[] userIds);

  @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
       + "last_name LIKE :last LIMIT 1")
  User findByName(String first, String last);

  @Insert
  void insertAll(User... users);

  @Delete
  void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

ฉันไม่แนะนำให้ Room กับฐานข้อมูลที่มีความสัมพันธ์แบบ N-to-N หลายตัวเนื่องจากวิธีนี้ไม่สามารถจัดการได้ดีและคุณต้องเขียนรหัสจำนวนมากเพื่อหาวิธีแก้ไขสำหรับความสัมพันธ์เหล่านี้
AlexPad

3

ฉันมีปัญหาบางอย่างฉันคิดว่าฉันเข้าใจว่าทำไมฉันจึงผิดพลาด

ผมได้เขียนระดับฐานข้อมูลกระดาษห่อซึ่งรวมถึงการclose()ที่เรียกว่าใกล้ผู้ช่วยเป็นกระจกของopen()ที่เรียกว่า getWriteableDatabase ContentProviderและจากนั้นได้ย้ายไปยัง แบบจำลองสำหรับContentProviderใช้ไม่ได้SQLiteDatabase.close()ซึ่งฉันคิดว่าเป็นเงื่อนงำที่ยิ่งใหญ่เนื่องจากรหัสใช้getWriteableDatabaseในบางกรณีฉันยังคงเข้าถึงโดยตรง (แบบสอบถามการตรวจสอบความถูกต้องของหน้าจอในหลักดังนั้นฉันจึงย้ายไปยังรุ่น getWriteableDatabase / rawQuery

ฉันใช้ซิงเกิลตันและมีความคิดเห็นลางสังหรณ์เล็กน้อยในเอกสารปิด

ปิดใด ๆวัตถุฐานข้อมูลเปิด

(Bolding ของฉัน)

ดังนั้นฉันจึงมีข้อผิดพลาดเป็นระยะ ๆ ที่ฉันใช้เธรดพื้นหลังเพื่อเข้าถึงฐานข้อมูลและพวกเขาทำงานในเวลาเดียวกันเป็นเบื้องหน้า

ดังนั้นฉันคิดว่าclose()บังคับให้ฐานข้อมูลปิดโดยไม่คำนึงถึงเธรดอื่น ๆ ที่มีการอ้างอิงดังนั้นclose()ตัวมันเองจึงไม่เพียงทำการยกเลิกการจับคู่getWriteableDatabaseแต่บังคับให้ปิดคำขอใด ๆ ที่เปิดอยู่ ส่วนใหญ่เวลานี้ไม่ใช่ปัญหาเนื่องจากรหัสเป็นเธรดเดียว แต่ในกรณีแบบมัลติเธรดมักจะมีโอกาสในการเปิดและปิดการซิงค์

มีความคิดเห็นที่อ่านที่อื่นที่อธิบายว่าอินสแตนซ์ของรหัส SqLiteDatabaseHelper นับจากนั้นเวลาเดียวที่คุณต้องการปิดคือที่ที่คุณต้องการสถานการณ์ที่คุณต้องการทำสำเนาสำรองและคุณต้องการบังคับให้การเชื่อมต่อทั้งหมดถูกปิดและบังคับให้ SqLite เขียนสิ่งที่แคชไว้ซึ่งอาจเป็นเรื่องเกี่ยวกับ - ในคำอื่น ๆ หยุดกิจกรรมฐานข้อมูลแอปพลิเคชันทั้งหมดปิดในกรณีที่ตัวช่วยหายไปติดตามทำกิจกรรมระดับไฟล์ใด ๆ (สำรอง / กู้คืน) จากนั้นเริ่มต้นใหม่อีกครั้ง

แม้ว่าจะเป็นความคิดที่ดีที่จะลองและปิดในแบบควบคุมความจริงก็คือ Android ขอสงวนสิทธิ์ในการทิ้ง VM ของคุณเพื่อให้การปิดใด ๆ ลดความเสี่ยงของการอัปเดตแคชที่ไม่ได้เขียน แต่ไม่สามารถรับประกันได้หากอุปกรณ์ ถูกเน้นและถ้าคุณปล่อยเคอร์เซอร์และอ้างอิงฐานข้อมูลได้อย่างถูกต้อง (ซึ่งไม่ควรเป็นสมาชิกแบบสแตติก) ผู้ช่วยจะปิดฐานข้อมูลต่อไป

ดังนั้นเวลาของฉันคือแนวทางคือ:

ใช้ getWriteableDatabase เพื่อเปิดจาก wrapper เดี่ยว (ฉันใช้คลาสแอปพลิเคชันที่ได้รับเพื่อให้บริบทแอปพลิเคชันจากสแตติกเพื่อแก้ไขความต้องการบริบท)

อย่าโทรเข้าใกล้โดยตรง

อย่าเก็บฐานข้อมูลผลลัพธ์ในวัตถุใด ๆ ที่ไม่มีขอบเขตที่ชัดเจนและพึ่งพาการนับการอ้างอิงเพื่อทริกเกอร์การปิดโดยนัย ()

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


0

ฉันรู้ว่าการตอบสนองล่าช้า แต่วิธีที่ดีที่สุดในการดำเนินการค้นหา sqlite ใน Android คือผ่านผู้ให้บริการเนื้อหาที่กำหนดเอง ด้วยวิธีนี้ UI ถูกแยกออกด้วยคลาสฐานข้อมูล (คลาสที่ขยายคลาส SQLiteOpenHelper) นอกจากนี้แบบสอบถามจะถูกดำเนินการในเธรดพื้นหลัง (Cursor Loader)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.