Quais são as melhores práticas para SQLite no Android?

694

Quais seriam as melhores práticas ao executar consultas em um banco de dados SQLite em um aplicativo Android?

É seguro executar inserções, exclusões e consultas selecionadas no doInBackground de uma AsyncTask? Ou devo usar o thread da interface do usuário? Suponho que as consultas ao banco de dados possam ser "pesadas" e não devem usar o thread da interface do usuário, pois podem bloquear o aplicativo - resultando em um ANR ( Aplicativo Não Respondendo ).

Se eu tiver várias AsyncTasks, elas devem compartilhar uma conexão ou abrir uma conexão cada?

Existem práticas recomendadas para esses cenários?

Vidar Vestnes
fonte
10
Faça o que fizer, lembre-se de higienizar suas entradas se o seu provedor de conteúdo (ou interface SQLite) estiver voltado para o público!
22612 Kristopher Micinski
37
Você definitivamente NÃO deveria estar acessando o banco de dados a partir do thread da interface do usuário, posso dizer isso.
Edward Falk
@EdwardFalk Por que não? Certamente existem casos de uso em que é válido fazer isso?
Michael
4
Se você fizer I / O, acessos de rede, etc. a partir do encadeamento da interface do usuário, o dispositivo inteiro congela até que a operação seja concluída. Se for concluído em 1/20 segundo, tudo bem. Se demorar mais, você terá uma experiência ruim do usuário.
Edward Falk

Respostas:

632

Inserções, atualizações, exclusões e leituras geralmente são aceitáveis ​​em vários segmentos, mas a resposta de Brad não está correta. Você precisa ter cuidado com a maneira como cria suas conexões e as utiliza. Há situações em que suas chamadas de atualização falharão, mesmo que seu banco de dados não seja corrompido.

A resposta básica.

O objeto SqliteOpenHelper mantém uma conexão de banco de dados. Parece oferecer a você uma conexão de leitura e gravação, mas realmente não. Ligue para somente leitura e você obterá a conexão com o banco de dados de gravação independentemente.

Portanto, uma instância auxiliar, uma conexão db. Mesmo se você usá-lo a partir de vários threads, uma conexão por vez. O objeto SqliteDatabase usa bloqueios java para manter o acesso serializado. Portanto, se 100 threads tiverem uma instância de banco de dados, as chamadas para o banco de dados em disco real serão serializadas.

Portanto, um auxiliar, uma conexão db, serializada no código java. Um thread, 1000 threads, se você usar uma instância auxiliar compartilhada entre eles, todo o seu código de acesso db será serial. E a vida é boa (ish).

Se você tentar gravar no banco de dados a partir de conexões distintas reais ao mesmo tempo, uma delas falhará. Não esperará até que o primeiro seja feito e depois escreva. Simplesmente não escreverá sua alteração. Pior, se você não chamar a versão correta de inserção / atualização no SQLiteDatabase, não receberá uma exceção. Você receberá uma mensagem no seu LogCat, e será isso.

Então, vários threads? Use um ajudante. Período. Se você SABE que apenas um segmento estará escrevendo, PODE poder usar várias conexões e suas leituras serão mais rápidas, mas cuidado com o comprador. Eu não testei muito.

Aqui está uma postagem de blog com muito mais detalhes e um aplicativo de exemplo.

Gray e eu estamos realmente encerrando uma ferramenta ORM, baseada em seu Ormlite, que funciona nativamente com implementações de banco de dados Android e segue a estrutura segura de criação / chamada que descrevo na postagem do blog. Isso deve sair muito em breve. Dê uma olhada.


Enquanto isso, há uma postagem no blog de acompanhamento:

Verifique também o garfo com 2point0 do exemplo de bloqueio mencionado anteriormente:

Kevin Galligan
fonte
2
Além disso, o suporte para Android do Ormlite pode ser encontrado em ormlite.sourceforge.net/sqlite_java_android_orm.html . Existem projetos de amostra, documentação e jarros.
Grey
1
Um segundo à parte. O código ormlite possui classes auxiliares que podem ser usadas para gerenciar instâncias do dbhelper. Você pode usar o material ormlite, mas não é necessário. Você pode usar as classes auxiliares apenas para fazer o gerenciamento de conexões.
precisa
31
Kāgii, obrigado pela explicação detalhada. Você poderia esclarecer uma coisa - eu entendo que você deve ter UM ajudante, mas também deve ter apenas uma conexão (ou seja, um objeto SqliteDatabase)? Em outras palavras, com que frequência você deve chamar getWritableDatabase? E igualmente importante quando você chama close ()?
Artem
3
Eu atualizei o código. O original foi perdido quando troquei os hosts do blog, mas adicionei um código de exemplo reduzido que demonstrará o problema. Além disso, como você gerencia a conexão única? Inicialmente, eu tinha uma solução muito mais complicada, mas desde então a alterei. Dê uma olhada aqui: touchlab.co/uncategorized/single-sqlite-connection
Kevin Galligan
é necessário descartar a conexão e onde fazê-lo?
tugce
188

Acesso simultâneo ao banco de dados

Mesmo artigo no meu blog (eu gosto de formatar mais)

Escrevi um pequeno artigo que descreve como tornar seguro o acesso ao seu thread de banco de dados do Android.


Supondo que você tenha seu próprio SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Agora você deseja gravar dados no banco de dados em threads separados.

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

Você receberá a seguinte mensagem no seu logcat e uma de suas alterações não será gravada.

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

Isso está acontecendo porque toda vez que você cria um novo objeto SQLiteOpenHelper, está realmente fazendo uma nova conexão com o banco de dados. Se você tentar gravar no banco de dados a partir de conexões distintas reais ao mesmo tempo, uma delas falhará. (da resposta acima)

Para usar o banco de dados com vários threads, precisamos garantir que estamos usando uma conexão com o banco de dados.

Vamos criar a classe singleton Database Manager, que manterá e retornará um único objeto 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();
    }

}

O código atualizado que grava dados no banco de dados em threads separados terá a seguinte aparência.

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

Isso trará a você outra falha.

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

Como estamos usando apenas uma conexão com o banco de dados, o método getDatabase () retorna a mesma instância do objeto SQLiteDatabase para Thread1 e Thread2 . O que está acontecendo, o Thread1 pode fechar o banco de dados, enquanto o Thread2 ainda o está usando. É por isso que temos a falha IllegalStateException .

Precisamos garantir que ninguém esteja usando o banco de dados e só depois fechá-lo. Algumas pessoas no stackoveflow recomendam nunca fechar seu SQLiteDatabase . Isso resultará na seguinte mensagem de logcat.

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

Amostra de trabalho

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

        }
    }

}

Use-o da seguinte maneira.

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

Toda vez que você precisar de banco de dados, chame o método openDatabase () da classe DatabaseManager . Dentro desse método, temos um contador, que indica quantas vezes o banco de dados é aberto. Se for igual a um, significa que precisamos criar uma nova conexão com o banco de dados; caso contrário, a conexão com o banco de dados já está criada.

O mesmo acontece no método closeDatabase () . Toda vez que chamamos esse método, o contador diminui; sempre que chega a zero, estamos fechando a conexão com o banco de dados.


Agora você deve poder usar seu banco de dados e ter certeza de que ele é seguro para threads.

Dmytro Danylyk
fonte
10
Eu sou uma daquelas pessoas que sugerem que você nunca feche o banco de dados. Você só recebe o erro "Vazamento encontrado" se abrir o banco de dados, não o feche e tente abrir novamente. Se você usar apenas um único auxiliar aberto e nunca fechar o banco de dados, não receberá esse erro. Se você achar o contrário, entre em contato (com o código). Eu tinha um post mais longo sobre isso em algum lugar, mas não consigo encontrá-lo. Pergunta respondida por commonsware aqui, que tipo de outguns nós dois no departamento pontos SO: stackoverflow.com/questions/7211941/...
Kevin Galligan
4
Mais pensamentos. # 1, eu criaria o auxiliar dentro do seu gerente. Pedindo problemas para tê-lo fora. O novo desenvolvedor pode chamar o ajudante diretamente por algum motivo louco. Além disso, se você precisar de um método init, lance uma exceção se a instância já existir. Aplicativos multi-db obviamente falharão como estão. # 2, por que o campo mDatabase? Está disponível no auxiliar. # 3, como seu primeiro passo para "nunca fechar", o que acontece com o banco de dados quando o aplicativo falha e não é "fechado"? Dica, nada. Tudo bem, porque o SQLite é super estável. Essa foi a etapa 1 para descobrir por que você não precisa fechá-la.
precisa
1
Algum motivo pelo qual você usa um método público de inicialização para chamar antes de obter a instância? Por que não ter um construtor privado chamado if(instance==null)? Você não tem escolha a não ser chamar inicializar toda vez; de que outra forma você saberia se foi inicializado ou não em outros aplicativos, etc.?
ChiefTwoPencils
1
initializeInstance()tem um parâmetro do tipo SQLiteOpenHelper, mas no seu comentário você mencionou o uso DatabaseManager.initializeInstance(getApplicationContext());. O que está acontecendo? Como isso pode funcionar?
faizal
2
@DmytroDanylyk "O DatabaseManager é um singleton seguro para threads, portanto pode ser usado em qualquer lugar", o que não é verdade em resposta à pergunta do virsir. Objetos não são processos cruzados compartilhados. O DatabaseManager não terá estado em um processo diferente, como em um adaptador de sincronização (android: process = ": sync")
whizzle
17
  • Use a Threadou AsyncTaskpara operações de execução longa (50ms +). Teste seu aplicativo para ver onde está. A maioria das operações (provavelmente) não requer um encadeamento, porque a maioria das operações (provavelmente) envolve apenas algumas linhas. Use um encadeamento para operações em massa.
  • Compartilhe uma SQLiteDatabaseinstância para cada banco de dados no disco entre threads e implemente um sistema de contagem para acompanhar as conexões abertas.

Existem práticas recomendadas para esses cenários?

Compartilhe um campo estático entre todas as suas classes. Eu costumava manter um singleton por perto para isso e outras coisas que precisam ser compartilhadas. Um esquema de contagem (geralmente usando AtomicInteger) também deve ser usado para garantir que você nunca feche o banco de dados mais cedo ou o deixe aberto.

Minha solução:

Para a versão mais atual, consulte https://github.com/JakarCo/databasemanager, mas tentarei manter o código atualizado aqui também. Se você quiser entender minha solução, consulte o código e leia minhas anotações. Minhas anotações são geralmente bastante úteis.

  1. copie / cole o código em um novo arquivo chamado DatabaseManager. (ou faça o download no github)
  2. estender DatabaseManagere implementar onCreatee onUpgradecomo você faria normalmente. Você pode criar várias subclasses de uma DatabaseManagerclasse para ter diferentes bancos de dados em disco.
  3. Instancie sua subclasse e ligue getDb()para usar a SQLiteDatabaseclasse.
  4. Ligue close()para cada subclasse instanciada

O código para copiar / colar :

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();
                }
        } 
    }
}
Reed
fonte
1
O que acontece quando você chama "close" e tenta reutilizar a classe? vai falhar? ou ele será reinicializado automaticamente para poder usar o banco de dados novamente?
desenvolvedor android
1
@androiddeveloper, Se você ligar close, precisará ligar opennovamente antes de usar a mesma instância da classe OU poderá criar uma nova instância. Já que no closecódigo, eu jogo db=null, você não seria capaz de usar o valor de retorno getDb(uma vez que seria null), então você deseja obter um NullPointerExceptionse você fez algo parecidomyInstance.close(); myInstance.getDb().query(...);
Reed
Por que não combinar getDb()e open()em um único método?
precisa saber é o seguinte
@Burdu, uma combinação de fornecer controle extra sobre o contador do banco de dados e um design ruim. Definitivamente, não é a melhor maneira de fazê-lo. Vou atualizá-lo em alguns dias.
Reed
@ Burdu, acabei de atualizar. Você pode obter o novo código aqui . Eu não testei, então, deixe-me saber se devo confirmar as alterações.
Reed
11

O banco de dados é muito flexível com multi-threading. Meus aplicativos atingem seus DBs de vários segmentos diferentes simultaneamente e funciona muito bem. Em alguns casos, tenho vários processos atingindo o banco de dados simultaneamente e isso também funciona bem.

Suas tarefas assíncronas - use a mesma conexão quando puder, mas se for necessário, não há problema em acessar o banco de dados a partir de tarefas diferentes.

Brad Hein
fonte
Além disso, você tem leitores e escritores em conexões diferentes ou eles devem compartilhar uma única conexão? Obrigado.
Gray
@ Grey - correto, eu deveria ter mencionado isso explicitamente. No que diz respeito às conexões, eu usaria a mesma conexão o máximo possível, mas como o bloqueio é tratado no nível do sistema de arquivos, você pode abri-lo várias vezes no código, mas eu usaria uma única conexão o máximo possível. O banco de dados sqlite do Android é muito flexível e perdoador.
Brad Hein
3
@ Grey, só queria postar informações atualizadas para pessoas que possam estar usando esse método. A documentação diz: Este método agora não faz nada. Não use.
Pijusn 16/09/12
3
Descobri que esse método falhou terrivelmente; estamos mudando para o ContentProvider para acessar de vários aplicativos. Teremos que fazer alguma concorrência em nossos métodos, mas isso deve corrigir qualquer problema nos processos que acessam os dados ao mesmo tempo.
JPM
2
Eu sei que isso é antigo, mas está incorreto. Ele pode funcionar bem para o acesso DB a partir de diferentes SQLiteDatabaseobjetos em diferentes AsyncTasks / Threads, mas, por vezes, levar a erros e é por isso SQLiteDatabase (linha 1297), usos Locks
Reed
7

A resposta do Dmytro funciona bem no meu caso. Eu acho que é melhor declarar a função como sincronizada. pelo menos para o meu caso, ele invocaria uma exceção de ponteiro nulo, caso contrário, por exemplo, getWritableDatabase ainda não retornou em um thread e o openDatabse chamado em outro thread enquanto isso.

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
gonglong
fonte
mDatabaseHelper.getWritableDatabase (); Isso não vai criar novo objeto de banco de dados
periférica
5

depois de lutar com isso por algumas horas, descobri que você só pode usar um objeto auxiliar de banco de dados por execução de banco de dados. Por exemplo,

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

ao contrário de:

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

criar um novo DBAdapter sempre que o loop iterar era a única maneira de colocar minhas seqüências de caracteres em um banco de dados através da minha classe auxiliar.

dell116
fonte
4

Meu entendimento das APIs SQLiteDatabase é que, caso você tenha um aplicativo multiencadeado, você não pode se dar ao luxo de ter mais de um objeto SQLiteDatabase apontando para um único banco de dados.

Definitivamente, o objeto pode ser criado, mas as inserções / atualizações falham se diferentes threads / processos (também) começarem a usar objetos SQLiteDatabase diferentes (como usamos no JDBC Connection).

A única solução aqui é ficar com 1 objeto SQLiteDatabase e sempre que um startTransaction () for usado em mais de um segmento, o Android gerencia o bloqueio em diferentes segmentos e permite que apenas um segmento por vez tenha acesso exclusivo à atualização.

Além disso, você pode fazer "Reads" no banco de dados e usar o mesmo objeto SQLiteDatabase em um thread diferente (enquanto outro thread escreve) e nunca haveria corrupção no banco de dados, ou seja, "read thread" não leria os dados do banco de dados até o " write thread "confirma os dados, embora ambos usem o mesmo objeto SQLiteDatabase.

Isso é diferente de como o objeto de conexão está no JDBC, onde se você distribuir (usar o mesmo) o objeto de conexão entre os threads de leitura e gravação, provavelmente também estaremos imprimindo dados não confirmados.

No aplicativo corporativo, tento usar verificações condicionais para que o thread da interface do usuário nunca precise esperar, enquanto o thread BG mantém o objeto SQLiteDatabase (exclusivamente). Eu tento prever ações da interface do usuário e adiar a execução do thread BG por 'x' segundos. Também é possível manter o PriorityQueue para gerenciar a distribuição de objetos de Conexão SQLiteDatabase, para que o Thread da UI o obtenha primeiro.

Swaroop
fonte
E o que você coloca nesse PriorityQueue - ouvintes (que desejam obter objeto de banco de dados) ou consultas SQL?
Pijusn 18/09/12
Eu não usei a abordagem de fila prioritária, mas essencialmente os segmentos "chamador".
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,. Isso nem sempre é verdade; se você iniciar o "thread de leitura" logo após o "thread de gravação", poderá não receber os dados atualizados recentemente (inseridos ou atualizados no thread de gravação). O encadeamento de leitura pode ler os dados antes do início do encadeamento de gravação. Isso acontece porque a operação de gravação ativa inicialmente o bloqueio reservado em vez do bloqueio exclusivo.
Amit Vikram Singh
4

Você pode tentar aplicar a nova abordagem de arquitetura anunciada no Google I / O 2017.

Ele também inclui a nova biblioteca ORM chamada Room

Ele contém três componentes principais: @Entity, @Dao e @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();
}
Zimbo Rodger
fonte
Não sugiro espaço para um banco de dados com vários relacionamentos N para N, porque ele não lida bem com essa abordagem e você precisa escrever muito código para encontrar uma solução alternativa para esses relacionamentos.
AlexPad 21/05/19
3

Tendo tido alguns problemas, acho que entendi por que estou errado.

Eu escrevi uma classe de wrapper de banco de dados que incluía uma close()chamada de helper close como um espelho open()chamada getWriteableDatabase e depois migramos para a ContentProvider. O modelo para ContentProvidernão usa, SQLiteDatabase.close()que eu acho que é uma grande pista, pois o código usa. getWriteableDatabaseEm alguns casos, eu ainda estava fazendo acesso direto (consultas de validação de tela principalmente, então migrei para um modelo getWriteableDatabase / rawQuery.

Eu uso um singleton e há um comentário ligeiramente ameaçador na documentação próxima

Feche qualquer objeto de banco de dados aberto

(meu negrito).

Por isso, tive falhas intermitentes nas quais uso threads em segundo plano para acessar o banco de dados e eles são executados ao mesmo tempo em primeiro plano.

Então, eu acho que close()força o banco de dados a fechar, independentemente de quaisquer outros threads que contenham referências - então close()ele não está simplesmente desfazendo a correspondência, getWriteableDatabasemas força o fechamento de quaisquer solicitações em aberto. Na maioria das vezes, isso não é um problema, pois o código é de thread único, mas em casos com vários threads, sempre há a chance de abrir e fechar a sincronização.

Depois de ler comentários em outro lugar que explica que a instância de código SqLiteDatabaseHelper conta, a única vez que você deseja fechar é onde deseja a situação em que deseja fazer uma cópia de backup e deseja forçar o fechamento de todas as conexões e forçar o SqLite a escreva qualquer material armazenado em cache que possa estar demorando - em outras palavras, interrompa todas as atividades do banco de dados de aplicativos, feche caso o Helper tenha perdido o controle, realize qualquer atividade no nível do arquivo (backup / restauração) e comece tudo de novo.

Embora pareça uma boa ideia tentar fechar de maneira controlada, a realidade é que o Android se reserva o direito de descartar sua VM para que qualquer fechamento reduza o risco de as atualizações em cache não serem gravadas, mas não pode ser garantido se o dispositivo está estressado e, se você liberou corretamente os cursores e as referências aos bancos de dados (que não devem ser membros estáticos), o ajudante fechará o banco de dados de qualquer maneira.

Então, minha opinião é que a abordagem é:

Use getWriteableDatabase para abrir a partir de um wrapper singleton. (Eu usei uma classe de aplicativo derivada para fornecer o contexto do aplicativo a partir de uma estática para resolver a necessidade de um contexto).

Nunca ligue diretamente para perto.

Nunca armazene o banco de dados resultante em qualquer objeto que não tenha um escopo óbvio e confie na contagem de referências para acionar um fechamento implícito ().

Se estiver manipulando o nível de arquivo, interrompa toda a atividade do banco de dados e, em seguida, feche para fechar caso haja um encadeamento descontrolado, supondo que você escreva transações apropriadas, para que o encadeamento descontrolado falhe e o banco de dados fechado tenha pelo menos transações apropriadas. do que potencialmente uma cópia no nível do arquivo de uma transação parcial.

Ian Spencer
fonte
0

Eu sei que a resposta está atrasada, mas a melhor maneira de executar consultas sqlite no android é através de um provedor de conteúdo personalizado. Dessa maneira, a interface do usuário é dissociada da classe do banco de dados (a classe que estende a classe SQLiteOpenHelper). Além disso, as consultas são executadas em um encadeamento em segundo plano (Cursor Loader).

Theo
fonte