Android 앱의 SQLite 데이터베이스에서 쿼리를 실행할 때 모범 사례로 간주되는 것은 무엇입니까?
AsyncTask의 doInBackground에서 삽입, 삭제 및 선택 쿼리를 실행하는 것이 안전합니까? 아니면 UI 스레드를 사용해야합니까? 데이터베이스 쿼리가 “무거울”수 있고 앱을 잠글 수 있으므로 UI 스레드를 사용하지 않아야한다고 가정합니다 ( 응용 프로그램 응답 없음 ).
여러 AsyncTask가있는 경우 연결을 공유해야합니까, 아니면 각각 연결을 열어야합니까?
이러한 시나리오에 대한 모범 사례가 있습니까?
답변
삽입, 업데이트, 삭제 및 읽기는 일반적으로 여러 스레드에서 정상이지만 Brad의 대답 은 정확하지 않습니다. 연결을 만들고 사용하는 방법에주의해야합니다. 데이터베이스가 손상되지 않은 경우에도 업데이트 호출이 실패하는 상황이 있습니다.
기본 답변.
SqliteOpenHelper 객체는 하나의 데이터베이스 연결을 유지합니다. 읽기 및 쓰기 연결을 제공하는 것처럼 보이지만 실제로는 그렇지 않습니다. 읽기 전용을 호출하면 관계없이 쓰기 데이터베이스 연결이 제공됩니다.
따라서 하나의 도우미 인스턴스, 하나의 db 연결. 여러 스레드에서 사용하더라도 한 번에 하나의 연결입니다. SqliteDatabase 객체는 액세스를 직렬화하기 위해 java 잠금을 사용합니다. 따라서 100 개의 스레드에 하나의 db 인스턴스가 있으면 실제 온 디스크 데이터베이스에 대한 호출이 직렬화됩니다.
따라서 하나의 도우미, 하나의 db 연결은 Java 코드로 직렬화됩니다. 하나의 스레드, 1000 개의 스레드. 그들 사이에 공유 된 하나의 헬퍼 인스턴스를 사용하는 경우 모든 DB 액세스 코드는 직렬입니다. 그리고 인생은 좋습니다 (ish).
실제 고유 연결에서 데이터베이스에 동시에 쓰려고하면 실패합니다. 첫 번째 작업이 완료 될 때까지 기다렸다가 쓸 수 없습니다. 단순히 변경 사항을 쓰지 않습니다. 게다가 SQLiteDatabase에서 올바른 버전의 삽입 / 업데이트를 호출하지 않으면 예외가 발생하지 않습니다. LogCat에 메시지가 표시 될 것입니다.
그래서 여러 스레드? 하나의 도우미를 사용하십시오. 기간. 하나의 스레드 만 작성한다는 것을 알고 있다면 여러 개의 연결을 사용할 수 있으며 읽기 속도는 빨라지지만 구매자는주의해야합니다. 나는 그렇게 많이 테스트하지 않았습니다.
보다 자세한 내용과 예제 앱이 포함 된 블로그 게시물이 있습니다.
- Android Sqlite 잠금 (업데이트 링크 6/18/2012)
- GitHub의 터치 랩 에 의한 안드로이드 데이터베이스 잠금 충돌 사례
그레이와 저는 Ormlite를 기반으로하는 ORM 툴을 실제로 정리하고 있는데,이 툴은 Android 데이터베이스 구현에서 기본적으로 작동하며 블로그 포스트에서 설명하는 안전한 생성 / 호출 구조를 따릅니다. 그것은 곧 나올 것입니다. 구경하다.
그 동안 후속 블로그 게시물이 있습니다.
또한 앞에서 언급 한 잠금 예제의 2point0 으로 포크를 점검하십시오 .
- GitHub에서 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에 다음과 같은 메시지가 표시되고 변경 사항 중 하나가 기록되지 않습니다.
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 () 메소드 는 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 () 메소드 에서도 마찬가지 입니다. 이 메소드를 호출 할 때마다 카운터가 줄어들고 0이 될 때마다 데이터베이스 연결이 닫힙니다.
이제 데이터베이스를 사용할 수 있고 스레드 안전 상태 여야합니다.
답변
Thread또는AsyncTask장기 실행 작업 (50ms +)에 또는 을 사용하십시오 . 앱을 테스트하여 어디에 있는지 확인하십시오. 대부분의 작업 (아마도)에는 스레드가 필요하지 않습니다. 대부분의 작업 (아마도)에는 몇 개의 행만 포함되기 때문입니다. 대량 작업에는 스레드를 사용하십시오.SQLiteDatabase스레드간에 디스크의 각 DB에 대해 하나의 인스턴스를 공유 하고 열린 연결을 추적하는 계산 시스템을 구현하십시오.
이러한 시나리오에 대한 모범 사례가 있습니까?
모든 클래스간에 정적 필드를 공유하십시오. 나는 그와 다른 것들을 공유하기 위해 싱글 톤을 유지했습니다. 데이터베이스를 조기에 닫거나 열지 않은 상태로 두려면 계산 체계 (일반적으로 AtomicInteger를 사용)를 사용해야합니다.
내 해결책 :
최신 버전은 https://github.com/JakarCo/databasemanager를 참조 하십시오. 그러나 여기에서도 코드를 최신 상태로 유지하려고합니다. 내 솔루션을 이해하려면 코드를보고 내 노트를 읽으십시오. 내 노트는 일반적으로 매우 유용합니다.
- 코드를이라는 새 파일에 복사 / 붙여 넣기하십시오
DatabaseManager. (또는 github에서 다운로드하십시오) - 평소처럼 확장
DatabaseManager하고 구현onCreate하며 디스크에 다른 데이터베이스를 갖기 위해onUpgrade한DatabaseManager클래스 의 여러 서브 클래스를 작성할 수 있습니다 . - 서브 클래스를 인스턴스화
getDb()하고SQLiteDatabase클래스 를 사용하도록 호출하십시오 . 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();
}
}
}
}
답변
데이터베이스는 멀티 스레딩으로 매우 유연합니다. 내 앱은 여러 스레드에서 동시에 DB를 공격했으며 정상적으로 작동합니다. 어떤 경우에는 DB에 동시에 여러 프로세스가 충돌하여 작동합니다.
비동기 작업-가능한 경우 동일한 연결을 사용하지만 필요한 경우 다른 작업에서 DB에 액세스해도 괜찮습니다.
답변
Dmytro의 대답은 제 경우에 잘 작동합니다. 함수를 동기화 된 것으로 선언하는 것이 좋습니다. 적어도 내 경우에는 그렇지 않으면 null 포인터 예외를 호출합니다. 예를 들어 getWritableDatabase가 아직 한 스레드에서 반환되지 않고 openDatabse가 다른 스레드 동안 호출되었습니다.
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
답변
몇 시간 동안이 문제로 어려움을 겪은 후 db 실행마다 하나의 db helper 객체 만 사용할 수 있음을 발견했습니다. 예를 들어
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에 대한 이해는 다중 스레드 응용 프로그램이있는 경우 단일 데이터베이스를 가리키는 둘 이상의 SQLiteDatabase 개체를 가질 여유가 없다는 것입니다.
다른 스레드 / 프로세스 (너무)가 다른 SQLiteDatabase 개체 (JDBC 연결에서 사용하는 방식)를 사용하기 시작하면 개체를 확실히 만들 수 있지만 삽입 / 업데이트가 실패합니다.
여기서 유일한 해결책은 하나의 SQLiteDatabase 객체를 고수하고 startTransaction ()이 둘 이상의 스레드에서 사용될 때마다 Android는 여러 스레드에서 잠금을 관리하고 한 번에 하나의 스레드 만 독점 업데이트 액세스 권한을 갖도록 허용합니다.
또한 데이터베이스에서 “읽기”를 수행하고 다른 스레드에서 동일한 SQLiteDatabase 개체를 사용할 수 있으며 (다른 스레드가 쓰는 동안) 데이터베이스 손상이 발생하지 않습니다. 즉 “읽기 스레드”는 데이터베이스에서 데이터를 읽을 때까지 ” 쓰레드는 “같은 SQLiteDatabase 객체를 사용하지만 데이터를 커밋합니다.
이는 읽기 스레드와 쓰기 스레드간에 연결 개체를 전달 (동일하게 사용)하면 커밋되지 않은 데이터도 인쇄하는 JDBC의 연결 개체와 다릅니다.
엔터프라이즈 응용 프로그램에서 BG 스레드가 SQLiteDatabase 개체를 독점적으로 보유하는 동안 UI 스레드가 기다릴 필요가 없도록 조건부 검사를 사용하려고합니다. UI 동작을 예측하고 BG 스레드가 ‘x’초 동안 실행되지 않도록합니다. 또한 UI 스레드가 먼저 가져 오도록 PrioriteQueue를 유지하여 SQLiteDatabase Connection 객체를 전달하는 것을 관리 할 수 있습니다.