Các thực tiễn tốt nhất cho SQLite trên Android là gì?


694

Điều gì sẽ được coi là thực tiễn tốt nhất khi thực hiện truy vấn trên cơ sở dữ liệu SQLite trong ứng dụng Android?

Có an toàn để chạy chèn, xóa và chọn truy vấn từ doInBackground của AsyncTask không? Hay tôi nên sử dụng UI Thread? Tôi cho rằng các truy vấn cơ sở dữ liệu có thể "nặng" và không nên sử dụng luồng UI vì nó có thể khóa ứng dụng - dẫn đến Ứng dụng không phản hồi (ANR).

Nếu tôi có một vài AsyncT task, họ có nên chia sẻ kết nối hay họ nên mở một kết nối?

Có thực hành tốt nhất cho các kịch bản này?


10
Dù bạn làm gì, hãy nhớ vệ sinh đầu vào nếu nhà cung cấp nội dung của bạn (hoặc giao diện SQLite) đối mặt công khai!
Kristopher Micinski

37
Bạn chắc chắn KHÔNG nên thực hiện truy cập db từ luồng UI, tôi có thể nói với bạn nhiều như vậy.
Edward Falk

@EdwardFalk Tại sao không? Chắc chắn có những trường hợp sử dụng mà nó hợp lệ để làm điều này?
Michael

4
Nếu bạn thực hiện bất kỳ I / O nào, truy cập mạng, v.v. từ luồng UI, toàn bộ thiết bị sẽ đóng băng cho đến khi hoàn thành thao tác. Nếu nó hoàn thành trong 1/20 giây, thì tốt. Nếu mất nhiều thời gian hơn, bạn đã có trải nghiệm người dùng tồi.
Edward Falk

Câu trả lời:


632

Chèn, cập nhật, xóa và đọc thường OK từ nhiều luồng, nhưng câu trả lời của Brad là không chính xác. Bạn phải cẩn thận với cách bạn tạo kết nối và sử dụng chúng. Có những tình huống mà các cuộc gọi cập nhật của bạn sẽ thất bại, ngay cả khi cơ sở dữ liệu của bạn không bị hỏng.

Câu trả lời cơ bản.

Đối tượng SqliteOpenHelper giữ một kết nối cơ sở dữ liệu. Nó dường như cung cấp cho bạn một kết nối đọc và viết, nhưng nó thực sự không. Gọi chỉ đọc và bạn sẽ nhận được kết nối cơ sở dữ liệu ghi bất kể.

Vì vậy, một ví dụ về trình trợ giúp, một kết nối db. Ngay cả khi bạn sử dụng nó từ nhiều luồng, một kết nối tại một thời điểm. Đối tượng SqliteDatabase sử dụng các khóa java để giữ quyền truy cập nối tiếp. Vì vậy, nếu 100 luồng có một cá thể db, các cuộc gọi đến cơ sở dữ liệu trên đĩa thực tế sẽ được tuần tự hóa.

Vì vậy, một trình trợ giúp, một kết nối db, được tuần tự hóa trong mã java. Một luồng, 1000 luồng, nếu bạn sử dụng một cá thể trợ giúp được chia sẻ giữa chúng, tất cả mã truy cập db của bạn là nối tiếp. Và cuộc sống là tốt (ish).

Nếu bạn cố gắng ghi vào cơ sở dữ liệu từ các kết nối riêng biệt thực tế cùng một lúc, người ta sẽ thất bại. Nó sẽ không đợi đến khi đầu tiên được thực hiện và sau đó viết. Nó chỉ đơn giản là không viết thay đổi của bạn. Tệ hơn, nếu bạn không gọi đúng phiên bản chèn / cập nhật trên SQLiteDatabase, bạn sẽ không gặp ngoại lệ. Bạn sẽ nhận được một tin nhắn trong LogCat của bạn, và đó sẽ là nó.

Vì vậy, nhiều chủ đề? Sử dụng một người trợ giúp. Giai đoạn = Stage. Nếu bạn BIẾT chỉ một chủ đề sẽ được viết, bạn có thể sử dụng nhiều kết nối và việc đọc của bạn sẽ nhanh hơn, nhưng người mua hãy cẩn thận. Tôi đã không kiểm tra nhiều như vậy.

Đây là một bài đăng blog với nhiều chi tiết hơn và một ứng dụng ví dụ.

Gray và tôi thực sự đang gói một công cụ ORM, dựa trên Ormlite của anh ấy, hoạt động tự nhiên với việc triển khai cơ sở dữ liệu Android và tuân theo cấu trúc gọi / tạo an toàn mà tôi mô tả trong bài đăng trên blog. Điều đó sẽ được ra rất sớm. Hãy xem.


Trong khi đó, có một bài đăng trên blog:

Đồng thời kiểm tra ngã ba bằng 2point0 của ví dụ khóa đã đề cập trước đó:


2
Bên cạnh đó, hỗ trợ Android của Ormlite có thể được tìm thấy tại ormlite.sourceforge.net/sqlite_java_android_orm.html . Có các dự án mẫu, tài liệu và lọ.
Xám

1
Một giây qua một bên. Mã ormlite có các lớp trình trợ giúp có thể được sử dụng để quản lý các thể hiện dbrcper. Bạn có thể sử dụng công cụ ormlite, nhưng không bắt buộc. Bạn có thể sử dụng các lớp của trình trợ giúp chỉ để thực hiện quản lý kết nối.
Kevin Galligan

31
Kāgii, cảm ơn bạn đã giải thích chi tiết. Bạn có thể làm rõ một điều - tôi hiểu bạn nên có MỘT người trợ giúp, nhưng bạn cũng chỉ nên có một kết nối (tức là một đối tượng SqliteDatabase)? Nói cách khác, bạn có thường xuyên gọi getWritableDatabase không? Và quan trọng không kém khi bạn gọi close ()?
Artem

3
Tôi đã cập nhật mã. Bản gốc đã bị mất khi tôi chuyển đổi máy chủ blog, nhưng tôi đã thêm một số mã ví dụ rút gọn sẽ chứng minh vấn đề. Ngoài ra, làm thế nào để bạn quản lý kết nối duy nhất? Tôi đã có một giải pháp phức tạp hơn nhiều ban đầu, nhưng tôi đã sửa đổi nó. Hãy xem tại đây: touchlab.co/uncated/single-sqlite-connection
Kevin Galligan

Có cần phải loại bỏ kết nối và làm ở đâu?
kéo

187

Truy cập cơ sở dữ liệu đồng thời

Cùng một bài viết trên blog của tôi (tôi thích định dạng hơn)

Tôi đã viết một bài viết nhỏ mô tả cách làm thế nào để truy cập vào chủ đề cơ sở dữ liệu Android của bạn an toàn.


Giả sử bạn có SQLiteOpenHelper của riêng bạn .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Bây giờ bạn muốn ghi dữ liệu vào cơ sở dữ liệu trong các luồng riêng biệt.

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

Bạn sẽ nhận được thông báo sau trong logcat của bạn và một trong những thay đổi của bạn sẽ không được viết.

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

Điều này xảy ra bởi vì mỗi khi bạn tạo đối tượng SQLiteOpenHelper mới, bạn thực sự tạo kết nối cơ sở dữ liệu mới. Nếu bạn cố gắng ghi vào cơ sở dữ liệu từ các kết nối riêng biệt thực tế cùng một lúc, người ta sẽ thất bại. (từ câu trả lời ở trên)

Để sử dụng cơ sở dữ liệu với nhiều luồng, chúng tôi cần đảm bảo rằng chúng tôi đang sử dụng một kết nối cơ sở dữ liệu.

Chúng ta hãy tạo Trình quản lý cơ sở dữ liệu lớp đơn sẽ giữ và trả về một đối tượng SQLiteOpenHelper duy nhất .

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

}

Mã cập nhật ghi dữ liệu vào cơ sở dữ liệu trong các luồng riêng biệt sẽ trông như thế này.

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

Điều này sẽ mang lại cho bạn một vụ tai nạn khác.

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

Vì chúng tôi chỉ sử dụng một kết nối cơ sở dữ liệu, phương thức getDatabase () trả về cùng phiên bản của đối tượng SQLiteDatabase cho Thread1Thread2 . Điều gì đang xảy ra, Thread1 có thể đóng cơ sở dữ liệu, trong khi Thread2 vẫn đang sử dụng nó. Đó là lý do tại sao chúng tôi gặp sự cố IllegalStateException .

Chúng ta cần đảm bảo không ai đang sử dụng cơ sở dữ liệu và chỉ sau đó đóng nó. Một số người trên stackoveflow được khuyến nghị không bao giờ đóng SQLiteDatabase của bạn . Điều này sẽ dẫn đến thông điệp logcat sau.

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

Mẫu làm việc

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

        }
    }

}

Sử dụng nó như sau.

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

Mỗi khi bạn cần cơ sở dữ liệu, bạn nên gọi phương thức openDatabase () của lớp DatabaseManager . Bên trong phương thức này, chúng ta có một bộ đếm, cho biết số lần cơ sở dữ liệu được mở. Nếu nó bằng một, điều đó có nghĩa là chúng ta cần tạo kết nối cơ sở dữ liệu mới, nếu không, kết nối cơ sở dữ liệu đã được tạo.

Điều tương tự cũng xảy ra trong phương thức closeDatabase () . Mỗi khi chúng ta gọi phương thức này, bộ đếm bị giảm, bất cứ khi nào nó về 0, chúng ta sẽ đóng kết nối cơ sở dữ liệu.


Bây giờ bạn sẽ có thể sử dụng cơ sở dữ liệu của mình và chắc chắn rằng chuỗi đó an toàn.


10
Tôi là một trong những người đề nghị bạn không bao giờ đóng db. Bạn chỉ gặp lỗi "Tìm thấy rò rỉ" nếu bạn mở db, không đóng nó, sau đó thử mở lại. Nếu bạn chỉ sử dụng một trình trợ giúp mở duy nhất và không bao giờ đóng db, bạn sẽ không gặp phải lỗi đó. Nếu bạn tìm thấy khác, xin vui lòng cho tôi biết (với mã). Tôi đã có một bài viết dài hơn về nó ở đâu đó, nhưng không thể tìm thấy nó. Câu hỏi được trả lời bởi commonsware ở đây, người loại chúng tôi cả trong bộ phận điểm SO: stackoverflow.com/questions/7211941/
Kẻ

4
Thêm suy nghĩ. # 1, tôi sẽ tạo người trợ giúp bên trong người quản lý của bạn. Yêu cầu các vấn đề để có nó bên ngoài. Nhà phát triển mới có thể gọi người trợ giúp trực tiếp vì một số lý do điên rồ. Ngoài ra, nếu bạn sẽ yêu cầu một phương thức init, hãy ném một ngoại lệ nếu thể hiện đã tồn tại. Các ứng dụng đa db rõ ràng sẽ thất bại. # 2, tại sao trường mDatabase? Nó có sẵn từ người trợ giúp. # 3, là bước đầu tiên của bạn đối với "không bao giờ đóng", điều gì xảy ra với db của bạn khi ứng dụng của bạn gặp sự cố và không "đóng"? Gợi ý, không có gì. Nó ổn, vì SQLite siêu ổn định. Đó là bước 1 để tìm ra lý do tại sao bạn không cần phải đóng nó.
Kevin Galligan

1
Bất kỳ lý do tại sao bạn sử dụng một phương thức khởi tạo công khai để gọi trước khi lấy ví dụ? Tại sao không có một nhà xây dựng tư nhân được gọi là if(instance==null)? Bạn không còn lựa chọn nào khác ngoài việc gọi khởi tạo mỗi lần; Làm thế nào khác bạn biết nếu nó đã được khởi tạo hay không trong các ứng dụng khác, v.v.?
ChiefTwoPsticks

1
initializeInstance()có một tham số của loại SQLiteOpenHelper, nhưng trong bình luận của bạn, bạn đã đề cập để sử dụng DatabaseManager.initializeInstance(getApplicationContext());. Chuyện gì đang xảy ra vậy? Làm thế nào điều này có thể làm việc?
faizal

2
@DmytroDanylyk "DatabaseManager là luồng đơn an toàn, vì vậy nó có thể được sử dụng ở bất cứ đâu" không đúng khi trả lời câu hỏi của virsir. Các đối tượng không được chia sẻ quá trình chéo. Cơ sở dữ liệu của bạn sẽ không có trạng thái trong một quy trình khác như trong bộ điều hợp đồng bộ hóa (android: process = ": sync")
whizzle

17
  • Sử dụng một Threadhoặc AsyncTaskcho các hoạt động dài hạn (50ms +). Kiểm tra ứng dụng của bạn để xem đó là nơi nào. Hầu hết các hoạt động (có thể) không yêu cầu một luồng, bởi vì hầu hết các hoạt động (có thể) chỉ liên quan đến một vài hàng. Sử dụng một chủ đề cho các hoạt động số lượng lớn.
  • Chia sẻ một SQLiteDatabaseví dụ cho mỗi DB trên đĩa giữa các luồng và thực hiện một hệ thống đếm để theo dõi các kết nối mở.

Có thực hành tốt nhất cho các kịch bản này?

Chia sẻ một trường tĩnh giữa tất cả các lớp của bạn. Tôi đã từng giữ một singleton xung quanh cho điều đó và những thứ khác cần được chia sẻ. Một lược đồ đếm (thường sử dụng AtomicInteger) cũng nên được sử dụng để đảm bảo bạn không bao giờ đóng cơ sở dữ liệu sớm hoặc để nó mở.

Giải pháp của tôi:

Để biết phiên bản mới nhất, hãy xem https://github.com/JakarCo/databasemanager nhưng tôi cũng sẽ cố gắng giữ mã cập nhật tại đây. Nếu bạn muốn hiểu giải pháp của tôi, hãy nhìn vào mã và đọc ghi chú của tôi. Ghi chú của tôi thường khá hữu ích.

  1. sao chép / dán mã vào một tệp mới có tên DatabaseManager. (hoặc tải xuống từ github)
  2. mở rộng DatabaseManagervà thực hiện onCreateonUpgradenhư bạn thường làm. Bạn có thể tạo nhiều lớp con của một DatabaseManagerlớp để có cơ sở dữ liệu khác nhau trên đĩa.
  3. Khởi tạo lớp con của bạn và gọi getDb()để sử dụng SQLiteDatabaselớp.
  4. Gọi close()cho mỗi lớp con bạn khởi tạo

Mã để sao chép / dán :

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
Điều gì xảy ra khi bạn gọi "đóng" và sau đó thử sử dụng lại lớp? nó sẽ sụp đổ? hoặc nó sẽ tự động khởi tạo lại chính nó để có thể sử dụng lại DB?
nhà phát triển Android

1
@androiddeveloper, Nếu bạn gọi close, bạn phải gọi openlại trước khi sử dụng cùng một thể hiện của lớp HOẶC bạn có thể tạo một thể hiện mới. Vì trong closemã, tôi đã đặt db=null, bạn sẽ không thể sử dụng giá trị trả về từ getDb(vì nó sẽ là null), vì vậy bạn sẽ nhận được NullPointerExceptionnếu bạn đã làm một cái gì đó nhưmyInstance.close(); myInstance.getDb().query(...);
Sậy

Tại sao không kết hợp getDb()open()thành một phương pháp duy nhất?
Alex Burdusel

@Burdu, Một kết hợp cung cấp kiểm soát bổ sung đối với bộ đếm cơ sở dữ liệu và thiết kế xấu. Đó chắc chắn không phải là cách tốt nhất để làm điều đó. Tôi sẽ cập nhật nó trong một vài ngày.
Sậy

@Burdu, tôi vừa cập nhật nó. Bạn có thể lấy mã mới từ đây . Tôi chưa thử nó, vì vậy xin vui lòng cho tôi biết nếu tôi nên thực hiện các thay đổi.
Sậy

11

Cơ sở dữ liệu rất linh hoạt với đa luồng. Các ứng dụng của tôi đạt DB của họ từ nhiều luồng khác nhau đồng thời và nó hoạt động tốt. Trong một số trường hợp, tôi có nhiều tiến trình nhấn DB đồng thời và nó cũng hoạt động tốt.

Các tác vụ không đồng bộ của bạn - sử dụng cùng một kết nối khi bạn có thể, nhưng nếu bạn phải, bạn có thể truy cập DB từ các tác vụ khác nhau.


Ngoài ra, bạn có độc giả và nhà văn trong các kết nối khác nhau hay họ nên chia sẻ một kết nối duy nhất? Cảm ơn.
Xám

@Gray - chính xác, tôi nên đã đề cập điều đó một cách rõ ràng. Theo như các kết nối, tôi sẽ sử dụng cùng một kết nối càng nhiều càng tốt, nhưng vì việc khóa được xử lý ở cấp hệ thống tập tin, bạn có thể mở nó nhiều lần trong mã, nhưng tôi sẽ sử dụng một kết nối càng nhiều càng tốt. Android sqlite DB rất linh hoạt và dễ tha thứ.
Brad Hein

3
@Gray, chỉ muốn đăng một thông tin cập nhật cho những người có thể đang sử dụng phương pháp này. Tài liệu nói: Phương pháp này bây giờ không làm gì cả. Không được dùng.
Pijusn

3
Tôi đã tìm thấy phương pháp này thất bại khủng khiếp, chúng tôi đang chuyển sang ContentProvider để truy cập từ nhiều ứng dụng. Chúng tôi sẽ phải thực hiện một số đồng thời về các phương thức của mình nhưng điều đó sẽ khắc phục mọi sự cố với tất cả các quy trình truy cập dữ liệu cùng một lúc.
JPM

2
Tôi biết điều này là cũ, nhưng nó không chính xác. Có thể hoạt động ổn khi truy cập DB từ các SQLiteDatabaseđối tượng khác nhau trên các AsyncTasks / s khác nhau Thread, nhưng đôi khi sẽ dẫn đến lỗi, đó là lý do tại sao SQLiteDatabase (dòng 1297) sử dụng Locks
Reed

7

Câu trả lời của Dmytro hoạt động tốt cho trường hợp của tôi. Tôi nghĩ tốt hơn là khai báo hàm là đồng bộ hóa. ít nhất là đối với trường hợp của tôi, nó sẽ gọi ngoại lệ con trỏ null, ví dụ, getWritableDatabase chưa được trả về trong một luồng và openDatabse được gọi trong một luồng khác trong khi đó.

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

mDatabaseHelper.getWritableDatabase (); Điều này sẽ không tạo đối tượng cơ sở dữ liệu mới
ngoại trừ

5

sau khi vật lộn với điều này trong một vài giờ, tôi đã thấy rằng bạn chỉ có thể sử dụng một đối tượng trợ giúp db cho mỗi lần thực thi db. Ví dụ,

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

như được gọi là:

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

tạo một DBAd CHƯƠNG mới mỗi lần vòng lặp lặp là cách duy nhất tôi có thể đưa các chuỗi của mình vào cơ sở dữ liệu thông qua lớp trình trợ giúp của mình.


4

Hiểu biết của tôi về API SQLiteDatabase là trong trường hợp bạn có một ứng dụng đa luồng, bạn không thể có nhiều hơn một đối tượng SQLiteDatabase chỉ vào một cơ sở dữ liệu.

Đối tượng chắc chắn có thể được tạo nhưng việc chèn / cập nhật thất bại nếu các luồng / quá trình khác nhau (cũng) bắt đầu sử dụng các đối tượng SQLiteDatabase khác nhau (như cách chúng tôi sử dụng trong Kết nối JDBC).

Giải pháp duy nhất ở đây là gắn bó với 1 đối tượng SQLiteDatabase và bất cứ khi nào startTransaction () được sử dụng trong hơn 1 luồng, Android sẽ quản lý khóa trên các luồng khác nhau và chỉ cho phép 1 luồng truy cập cập nhật độc quyền.

Ngoài ra, bạn có thể thực hiện "Đọc" từ cơ sở dữ liệu và sử dụng cùng một đối tượng SQLiteDatabase trong một luồng khác (trong khi một luồng khác ghi) và sẽ không bao giờ có lỗi cơ sở dữ liệu tức là "đọc luồng" sẽ không đọc dữ liệu từ cơ sở dữ liệu cho đến khi " write thread "cam kết dữ liệu mặc dù cả hai đều sử dụng cùng một đối tượng SQLiteDatabase.

Điều này khác với cách đối tượng kết nối trong JDBC trong đó nếu bạn chuyển qua (sử dụng giống nhau) đối tượng kết nối giữa các luồng đọc và ghi thì chúng ta cũng có thể sẽ in dữ liệu không được cam kết.

Trong ứng dụng doanh nghiệp của mình, tôi cố gắng sử dụng các kiểm tra có điều kiện để Giao diện người dùng không bao giờ phải chờ, trong khi luồng BG giữ đối tượng SQLiteDatabase (độc quyền). Tôi cố gắng dự đoán các Tác vụ UI và trì hoãn luồng BG chạy trong 'x' giây. Ngoài ra, người ta có thể duy trì PriorityQueue để quản lý việc phân phát các đối tượng Kết nối SQLiteDatabase để UI Thread lấy nó trước.


Và bạn đặt gì vào PriorityQueue - người nghe (muốn lấy đối tượng cơ sở dữ liệu) hoặc truy vấn SQL?
Pijusn

Tôi không sử dụng cách tiếp cận hàng đợi ưu tiên, nhưng về cơ bản là các luồng "người gọi".
Swaroop

@Swaroop: PCMIIW , "read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object. Điều này không phải lúc nào cũng đúng, nếu bạn bắt đầu "đọc luồng" ngay sau khi "ghi luồng", bạn có thể không nhận được dữ liệu mới được cập nhật (được chèn hoặc cập nhật trong luồng ghi). Đọc chủ đề có thể đọc dữ liệu trước khi bắt đầu viết chủ đề. Điều này xảy ra bởi vì hoạt động ghi ban đầu cho phép khóa dành riêng thay vì khóa độc quyền.
Amit Vikram Singh

4

Bạn có thể thử áp dụng cách tiếp cận kiến trúc mới anounced tại Google I / O năm 2017.

Nó cũng bao gồm thư viện ORM mới gọi là Room

Nó chứa ba thành phần chính: @Entity, @Dao và @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.
}

Người dùngDao.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();
}

Tôi không đề xuất Phòng cho Cơ sở dữ liệu có nhiều mối quan hệ N-N, vì nó không xử lý tốt phương pháp này và bạn phải viết rất nhiều mã để tìm cách giải quyết cho các mối quan hệ này.
AlexPad

3

Có một số vấn đề, tôi nghĩ rằng tôi đã hiểu tại sao tôi đã đi sai.

Tôi đã viết một lớp trình bao bọc cơ sở dữ liệu bao gồm một lớp close()được gọi là trình trợ giúp đóng như một bản sao trong open()đó gọi là getWritizableDatabase và sau đó đã chuyển sang a ContentProvider. Mô hình ContentProviderkhông sử dụng SQLiteDatabase.close()mà tôi nghĩ là một đầu mối lớn vì mã này sử dụng getWriteableDatabaseTrong một số trường hợp, tôi vẫn đang truy cập trực tiếp (truy vấn xác thực màn hình trong chính vì vậy tôi đã di chuyển sang mô hình getWritizableDatabase / rawQuery.

Tôi sử dụng một singleton và có một nhận xét hơi đáng ngại trong tài liệu đóng

Đóng mọi đối tượng cơ sở dữ liệu mở

(khuôn của tôi).

Vì vậy, tôi đã gặp sự cố gián đoạn khi tôi sử dụng các luồng nền để truy cập cơ sở dữ liệu và chúng chạy cùng lúc với tiền cảnh.

Vì vậy, tôi nghĩ rằng close()buộc cơ sở dữ liệu phải đóng bất kể các luồng khác đang giữ các tham chiếu - vì vậy close()bản thân nó không chỉ đơn giản là hoàn tác khớp getWriteableDatabasemà buộc đóng bất kỳ yêu cầu mở nào . Hầu hết thời gian, đây không phải là vấn đề vì mã là một luồng đơn, nhưng trong các trường hợp đa luồng, luôn có cơ hội mở và đóng không đồng bộ.

Đã đọc các bình luận ở nơi khác giải thích rằng thể hiện mã SqLiteDatabaseHelper, sau đó lần duy nhất bạn muốn đóng là bạn muốn thực hiện một bản sao dự phòng và bạn muốn buộc tất cả các kết nối bị đóng và buộc SqLite phải đóng ghi lại mọi nội dung được lưu trong bộ nhớ cache - nói cách khác là dừng tất cả hoạt động của cơ sở dữ liệu ứng dụng, đóng lại trong trường hợp Trình trợ giúp bị mất dấu, thực hiện bất kỳ hoạt động nào ở cấp độ tệp (sao lưu / khôi phục) rồi bắt đầu lại.

Mặc dù nghe có vẻ là một ý tưởng tốt để thử và đóng theo kiểu có kiểm soát, nhưng thực tế là Android có quyền bỏ rác VM của bạn để mọi đóng cửa đều giảm nguy cơ cập nhật bộ nhớ cache không được ghi, nhưng nó không thể được đảm bảo nếu thiết bị được nhấn mạnh và nếu bạn đã giải phóng chính xác các con trỏ và tham chiếu đến cơ sở dữ liệu (không phải là thành viên tĩnh) thì trình trợ giúp sẽ vẫn đóng cơ sở dữ liệu.

Vì vậy, tôi nghĩ rằng cách tiếp cận là:

Sử dụng getWrititableDatabase để mở từ trình bao bọc đơn. (Tôi đã sử dụng một lớp ứng dụng dẫn xuất để cung cấp bối cảnh ứng dụng từ tĩnh để giải quyết nhu cầu về bối cảnh).

Không bao giờ gọi trực tiếp gần.

Không bao giờ lưu trữ cơ sở dữ liệu kết quả trong bất kỳ đối tượng nào không có phạm vi rõ ràng và dựa vào đếm tham chiếu để kích hoạt một đóng ().

Nếu thực hiện xử lý mức tệp, tạm dừng tất cả hoạt động cơ sở dữ liệu và sau đó gọi đóng lại trong trường hợp có luồng xử lý giả định rằng bạn viết các giao dịch phù hợp để luồng chạy sẽ thất bại và cơ sở dữ liệu đã đóng ít nhất sẽ có giao dịch đúng hơn khả năng là một bản sao cấp độ tập tin của một giao dịch một phần.


0

Tôi biết rằng phản hồi là muộn, nhưng cách tốt nhất để thực hiện các truy vấn sqlite trong Android là thông qua một nhà cung cấp nội dung tùy chỉnh. Theo cách đó, UI được tách rời với lớp cơ sở dữ liệu (lớp mở rộng lớp SQLiteOpenHelper). Ngoài ra, các truy vấn được thực hiện trong một luồng nền (Trình tải con trỏ).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.