Androidアプリ内でSQLiteデータベースに対してクエリを実行する際のベストプラクティスとは何でしょうか。
AsyncTaskのdoInBackgroundから挿入、削除、およびクエリの選択を実行しても安全ですか?それとも私たちはUIスレッドを使うべきですか?データベースクエリは "重い"ものになり、アプリケーションをロックする可能性があるためUIスレッドを使用しないでください。その結果、 Application Not Responding (ANR)が発生します。
複数のAsyncTaskがある場合、それらは接続を共有するべきですか、それともそれぞれ接続を開くべきですか?
これらのシナリオに対するベストプラクティスはありますか?
挿入、更新、削除、および読み込みは通常、複数のスレッドから問題ありませんが、Bradの answer は正しくありません。接続をどのように作成して使用するかには注意が必要です。データベースが破損しなくても、更新呼び出しが失敗する状況があります。
基本的な答えです。
SqliteOpenHelperオブジェクトは1つのデータベース接続を保持します。読み書き接続を提供するように見えますが、実際にはそうではありません。読み取り専用を呼び出すと、それに関係なく書き込みデータベース接続が確立されます。
つまり、1つのヘルパーインスタンス、1つのdb接続です。あなたが複数のスレッドからそれを使用するとしても、一度に一つの接続。 SqliteDatabaseオブジェクトは、アクセスを直列化し続けるためにJavaロックを使用します。したがって、100個のスレッドに1つのdbインスタンスがある場合、実際のディスク上のデータベースへの呼び出しはシリアル化されます。
つまり、1つのヘルパー、1つのdb接続、これはJavaコードで直列化されています。 1つのスレッド、1000のスレッド、それらの間で共有されている1つのヘルパーインスタンスを使用する場合、すべてのdbアクセスコードはシリアルです。そして人生はいいですね。
実際の異なる接続から同時にデータベースに書き込もうとすると失敗します。最初の処理が完了してから書き込むまで待機しません。それは単にあなたの変更を書かないでしょう。さらに悪いことに、SQLiteDatabaseで正しいバージョンのinsert/updateを呼び出さなければ、例外は発生しません。 LogCatにメッセージが表示されれば、それで終わりです。
それで、マルチスレッド? 1人の助手を使ってください。期間。 1つのスレッドだけが書いていることを知っているなら、あなたは複数の接続を使うことができるかもしれません、そして、あなたの読み取りは速くなるでしょう、しかしバイヤーは用心します。私はそれほどテストしていません。
これは、はるかに詳細なブログ記事とサンプルアプリケーションです。
Greyと私は実際、彼のOrmliteをベースにしたAndroidデータベース実装とネイティブに連携し、ブログ記事で説明している安全な作成/呼び出し構造に従うORMツールをまとめています。それはすぐに出るはずです。見てください。
それまでの間、フォローアップのブログ記事があります。
前述のロックの例の2point0でフォークをチェックアウトします。
私はあなたの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の中に次のメッセージを受け取るでしょう、そしてあなたの変更の1つは書かれないでしょう。
Android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
これは、新しいSQLiteOpenHelperオブジェクトを作成するたびに、実際に新しいデータベース接続を確立しているためです。実際の異なる接続から同時にデータベースに書き込もうとすると失敗します。 (上記の回答から)
データベースを複数のスレッドで使用するには、1つのデータベース接続を使用していることを確認する必要があります。
シングルトンクラスデータベースマネージャを作成しましょう。これは単一の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
データベース接続を1つだけ使用しているので、メソッドgetDatabase()はThread1とThread2に対して同じ{SQLiteDatabaseオブジェクトのインスタンスを返します。何が起きているのですか、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
データベースが必要になるたびに、DatabaseManagerクラスのopenDatabase()メソッドを呼び出す必要があります。このメソッド内には、データベースが開かれた回数を示すカウンターがあります。それが1に等しい場合、それは私達が新しいデータベース接続を作成する必要があることを意味します、そうでなければ、データベース接続は既に作成されています。
closeDatabase()メソッドでも同じことが起こります。このメソッドを呼び出すたびに、counterが減らされ、ゼロになるたびにデータベース接続が閉じられます。
データベースを使用できるようになり、スレッドセーフであることを確認できます。
Thread
またはAsyncTask
を使用してください。アプリをテストして、それがどこにあるのかを確認します。ほとんどの操作は(おそらく)数行しか含まないため、ほとんどの操作は(おそらく)スレッドを必要としません。一括操作にはスレッドを使用してください。SQLiteDatabase
インスタンスを共有し、開いている接続を追跡するためのカウントシステムを実装します。これらのシナリオに対するベストプラクティスはありますか?
すべてのクラス間で静的フィールドを共有します。私はそれを共有する必要がある他のもののためにシングルトンを維持していました。データベースを早めに閉じたり、開いたままにしたりしないように、カウントスキーム(通常AtomicIntegerを使用)も使用する必要があります。
私の解決策:
最新版については、 https://github.com/JakarCo/databasemanager を参照してください。ただし、ここでもコードを最新の状態に保つようにします。私の解決策を理解したいのなら、コードを見て私のノートを読んでください。私のメモは通常とても役に立ちます。
DatabaseManager
という名前の新しいファイルに貼り付けます。 (またはgithubからダウンロードしてください)DatabaseManager
を拡張してonCreate
とonUpgrade
を実装します。ディスク上に異なるデータベースを配置するために、1つのDatabaseManager
クラスのサブクラスを複数作成することができます。SQLiteDatabase
クラスを使用するためにgetDb()
を呼び出します。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 [email protected] (or [email protected] )
*
* 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();
}
}
}
}
データベースはマルチスレッドで非常に柔軟です。私のアプリは、同時に多くの異なるスレッドから自分のDBにアクセスしていますが、それは問題ありません。場合によっては、複数のプロセスが同時にDBにアクセスしていて、それもうまく機能します。
あなたの非同期タスク - できる限り同じ接続を使用しますが、必要な場合は、別のタスクからDBにアクセスしても問題ありません。
Dmytroの答えは私の場合はうまくいきます。私はそれが同期として関数を宣言する方が良いと思います。少なくとも私の場合は、そうでなければnullポインタ例外を呼び出します。 getWritableDatabaseがあるスレッドでまだ返されておらず、openDatabseが別のスレッドで呼び出されています。
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
数時間これに苦労した後、私はあなたがdbの実行ごとに1つの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を作成することが、私のヘルパークラスを通して文字列をデータベースに入れる唯一の方法でした。
私がSQLiteDatabase APIを理解しているのは、マルチスレッドのアプリケーションを使用している場合、1つのデータベースを指すSQLiteDatabaseオブジェクトを複数持つことはできないということです。
確実にオブジェクトを作成することができますが、異なるスレッド/プロセス(異なる場合もあります)が異なるSQLiteDatabaseオブジェクトを使用し始めると、挿入/更新は失敗します(JDBC接続で使用する方法など)。
ここでの唯一の解決策は、1つのSQLiteDatabaseオブジェクトを使用することです。startTransaction()が複数のスレッドで使用されているときはいつでも、Androidは異なるスレッド間のロックを管理し、一度に1つのスレッドのみに排他更新アクセスを許可します。
また、データベースから "Reads"を実行して別のスレッドで同じSQLiteDatabaseオブジェクトを使用すると(別のスレッドが書き込み中)、データベースが破損することはありません。つまり、 "read thread"はデータベースからデータを読み取りません。どちらも同じSQLiteDatabaseオブジェクトを使用しますが、 "write thread"はデータをコミットします。
これは、読み込みスレッドと書き込みスレッドの間で接続オブジェクトを渡して(同じ方法で)使用すると、コミットされていないデータも印刷される可能性があるJDBCの接続オブジェクトとは異なります。
私のエンタープライズアプリケーションでは、条件付きチェックを使用してUIスレッドが待機する必要がないようにしていますが、BGスレッドはSQLiteDatabaseオブジェクトを(排他的に)保持しています。私は、UIアクションを予測し、BGスレッドの実行を「x」秒間遅らせるようにしています。また、UIスレッドが最初に取得するようにSQLiteDatabase Connectionオブジェクトの配布を管理するためにPriorityQueueを維持することもできます。
Google I/O 2017で、新しいアーキテクチャアプローチ anounced を適用してみることができます。
Room という新しいORMライブラリも含まれています。
@Entity、@ Dao、および@Databaseという3つの主要コンポーネントが含まれています。
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();
}
いくつかの問題がありましたが、私はなぜ私が間違っているのか理解したと思います。
私は、getWriteableDatabaseを呼び出すclose()
のミラーとしてcloseを呼び出すopen()
を含むデータベースラッパークラスを作成し、それからContentProvider
に移行しました。 ContentProvider
のモデルではSQLiteDatabase.close()
を使用していませんが、コードではgetWriteableDatabase
を使用しているので大きな手がかりになると思います。場合によってはまだ直接アクセス(画面検証クエリをメイン)していたので、getWriteableDatabase/rawQueryモデルに移行しました。
シングルトンを使用していますが、クローズドキュメントに少し不吉なコメントがあります
データベースオブジェクトを閉じる 任意 開く
(私の太字).
そのため、データベースにアクセスするためにバックグラウンドスレッドを使用し、それらがフォアグラウンドと同時に実行されるという断続的なクラッシュがありました。
それで私はclose()
が参照を保持している他のどのスレッドにもかかわらずデータベースを強制的に閉じると思います - それでclose()
自体は単に一致するgetWriteableDatabase
を元に戻すのではなく強制的に any openリクエストを閉じます。ほとんどの場合、コードはシングルスレッドであるためこれは問題にはなりませんが、マルチスレッドの場合は常に同期がずれる可能性があります。
SqLiteDatabaseHelperコードインスタンスが重要であることを説明している他の場所でコメントを読んだ後、あなたが唯一のあなたがあなたがバックアップコピーをしたい状況が欲しいところであり、そしてあなたはSqLiteに強制的にSqLiteを強制したい厄介な可能性があるキャッシュされたものをすべて削除します。言い換えると、ヘルパーが追跡を失った場合に備えて、すべてのアプリケーションデータベースのアクティビティを停止し、ファイルレベルのアクティビティ(バックアップ/復元)を実行します。
管理された方法で試して閉じるのは良い考えのように思えますが、現実にはAndroidはあなたのVMをゴミ箱に捨てる権利を留保しています。デバイスにストレスがかかっていて、カーソルとデータベースへの参照(静的メンバーではないはずです)を正しく解放していれば、ヘルパーはデータベースを閉じてしまいます。
私の考えでは、そのアプローチは次のようになります。
シングルトンラッパーから開くには、getWriteableDatabaseを使用してください。 (コンテキストの必要性を解決するために、静的からアプリケーションコンテキストを提供するために派生アプリケーションクラスを使用しました)。
直接電話をかけないでください。
明白なスコープを持たないオブジェクトに結果のデータベースを決して保存せず、暗黙のclose()を引き起こすために参照カウントに頼ります。
ファイルレベルの処理を行う場合は、適切なトランザクションを作成すると仮定して暴走スレッドがある場合に備えて、すべてのデータベースアクティビティを停止してからcloseを呼び出して、暴走スレッドが失敗し、閉じられたデータベースが少なくとも適切なトランザクションを保持するようにします。部分的なトランザクションのファイルレベルのコピーよりも潜在的に。
応答が遅いことを私は知っていますが、AndroidでSQLiteクエリを実行するための最良の方法はカスタムコンテンツプロバイダーを使用することです。そのようにして、UIはデータベースクラス(SQLiteOpenHelperクラスを拡張するクラス)と切り離されます。また、クエリはバックグラウンドスレッド(Cursor Loader)で実行されます。