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?
Respostas:
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:
fonte
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 .
Agora você deseja gravar dados no banco de dados em threads separados.
Você receberá a seguinte mensagem no seu logcat e uma de suas alterações não será gravada.
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 .
O código atualizado que grava dados no banco de dados em threads separados terá a seguinte aparência.
Isso trará a você outra falha.
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.
Amostra de trabalho
Use-o da seguinte maneira.
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.
fonte
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.?initializeInstance()
tem um parâmetro do tipoSQLiteOpenHelper
, mas no seu comentário você mencionou o usoDatabaseManager.initializeInstance(getApplicationContext());
. O que está acontecendo? Como isso pode funcionar?Thread
ouAsyncTask
para 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.SQLiteDatabase
instância para cada banco de dados no disco entre threads e implemente um sistema de contagem para acompanhar as conexões abertas.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.
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.
DatabaseManager
. (ou faça o download no github)DatabaseManager
e implementaronCreate
eonUpgrade
como você faria normalmente. Você pode criar várias subclasses de umaDatabaseManager
classe para ter diferentes bancos de dados em disco.getDb()
para usar aSQLiteDatabase
classe.close()
para cada subclasse instanciadaO código para copiar / colar :
fonte
close
, precisará ligaropen
novamente antes de usar a mesma instância da classe OU poderá criar uma nova instância. Já que noclose
código, eu jogodb=null
, você não seria capaz de usar o valor de retornogetDb
(uma vez que seria null), então você deseja obter umNullPointerException
se você fez algo parecidomyInstance.close(); myInstance.getDb().query(...);
getDb()
eopen()
em um único método?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.
fonte
SQLiteDatabase
objetos em diferentesAsyncTask
s /Thread
s, mas, por vezes, levar a erros e é por isso SQLiteDatabase (linha 1297), usosLock
sA 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.
fonte
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,
ao contrário de:
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.
fonte
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.
fonte
"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.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
UserDao.java
AppDatabase.java
fonte
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 espelhoopen()
chamada getWriteableDatabase e depois migramos para aContentProvider
. O modelo paraContentProvider
não usa,SQLiteDatabase.close()
que eu acho que é uma grande pista, pois o código usa.getWriteableDatabase
Em 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
(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ãoclose()
ele não está simplesmente desfazendo a correspondência,getWriteableDatabase
mas 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.
fonte
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).
fonte