Salvar dados em um banco de dados é ideal para dados estruturados ou que se repetem, por exemplo, os dados de contato. Esta página supõe que você esteja familiarizado com os bancos de dados SQL em geral e ajuda a começar a trabalhar com bancos de dados SQLite no Android. As APIs necessárias para usar um banco de dados no Android estão disponíveis no pacote android.database.sqlite
.
Cuidado: embora essas APIs sejam poderosas, elas são de nível bastante baixo e exigem muito tempo e esforço para serem usadas:
- Não há verificação em tempo de compilação de consultas SQL brutas. À medida que seu gráfico de dados for alterado, será necessário atualizar as consultas SQL afetadas manualmente. Esse processo pode ser demorado e propenso a erros.
- É necessário usar muito código boilerplate para converter consultas SQL em objetos de dados.
Por esses motivos, é altamente recomendável usar a Biblioteca de persistência do Room como uma camada de abstração para acessar informações em bancos de dados SQLite do app.
Definir um esquema e um contrato
Um dos princípios mais importantes dos bancos de dados SQL é o esquema: uma declaração formal de como o banco de dados é organizado. O esquema é refletido nas declarações SQL usadas na criação do banco de dados. É aconselhável criar uma classe de acompanhamento, conhecida como classe de contrato, que especifica explicitamente o layout do esquema de forma sistemática e autodocumentada.
Uma classe de contrato é o contêiner das constantes que definem nomes para URIs, tabelas e colunas. A classe de contrato permite usar as mesmas constantes em outras classes no mesmo pacote. Isso permite que você altere o nome de uma coluna em um local e que a mudança se propague por todo o código.
Uma boa forma de organizar uma classe de contrato é colocar definições que sejam globais para todo o banco de dados no nível raiz da classe. Em seguida, crie uma classe interna para cada tabela. Cada classe interna enumera as colunas da tabela correspondente.
Observação: ao implementar a interface BaseColumns
, sua classe interna pode herdar um campo-chave principal chamado _ID
que algumas classes do Android, como CursorAdapter
, esperam que ela tenha. Ela não é obrigatória, mas pode ajudar o banco de dados a trabalhar de forma mais harmoniosa com o framework do Android.
Por exemplo, o contrato a seguir define o nome da tabela e os nomes das colunas de uma única tabela que representa um feed RSS.
Kotlin
object FeedReaderContract { // Table contents are grouped together in an anonymous object. object FeedEntry : BaseColumns { const val TABLE_NAME = "entry" const val COLUMN_NAME_TITLE = "title" const val COLUMN_NAME_SUBTITLE = "subtitle" } }
Java
public final class FeedReaderContract { // To prevent someone from accidentally instantiating the contract class, // make the constructor private. private FeedReaderContract() {} /* Inner class that defines the table contents */ public static class FeedEntry implements BaseColumns { public static final String TABLE_NAME = "entry"; public static final String COLUMN_NAME_TITLE = "title"; public static final String COLUMN_NAME_SUBTITLE = "subtitle"; } }
Criar um banco de dados usando um SQL Helper
Uma vez definida a estrutura do banco de dados, implemente métodos que criam e mantêm o banco de dados e as tabelas. Veja algumas declarações comuns para criar e excluir uma tabela:
Kotlin
private const val SQL_CREATE_ENTRIES = "CREATE TABLE ${FeedEntry.TABLE_NAME} (" + "${BaseColumns._ID} INTEGER PRIMARY KEY," + "${FeedEntry.COLUMN_NAME_TITLE} TEXT," + "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)" private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"
Java
private static final String SQL_CREATE_ENTRIES = "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" + FeedEntry._ID + " INTEGER PRIMARY KEY," + FeedEntry.COLUMN_NAME_TITLE + " TEXT," + FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
Da mesma forma que você salva arquivos no armazenamento interno do dispositivo, o Android armazena seu banco de dados na pasta privada do app. Seus dados ficam protegidos porque, por padrão, essa área não pode ser acessada por outros apps nem pelo usuário.
A classe SQLiteOpenHelper
contém um conjunto de APIs útil para gerenciar seu banco de dados.
Quando você usa essa classe para conseguir referências para o banco de dados, o sistema realiza operações de execução possivelmente longas para criar e atualizar o banco de dados apenas quando necessário e não durante a inicialização do app. Basta chamar
getWritableDatabase()
ou
getReadableDatabase()
.
Observação: como esses métodos podem ser de longa duração, chame getWritableDatabase()
ou getReadableDatabase()
em uma linha de execução em segundo plano.
Consulte Linhas de execução no Android para mais informações.
Para usar SQLiteOpenHelper
, crie uma subclasse que
modifique o onCreate()
e os métodos de callback
onUpgrade()
. Você também pode implementar os métodos onDowngrade()
ou onOpen()
, mas eles não são obrigatórios.
Por exemplo, veja uma implementação do SQLiteOpenHelper
que utilize alguns dos comandos mostrados acima:
Kotlin
class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { override fun onCreate(db: SQLiteDatabase) { db.execSQL(SQL_CREATE_ENTRIES) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // This database is only a cache for online data, so its upgrade policy is // to simply to discard the data and start over db.execSQL(SQL_DELETE_ENTRIES) onCreate(db) } override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { onUpgrade(db, oldVersion, newVersion) } companion object { // If you change the database schema, you must increment the database version. const val DATABASE_VERSION = 1 const val DATABASE_NAME = "FeedReader.db" } }
Java
public class FeedReaderDbHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "FeedReader.db"; public FeedReaderDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_ENTRIES); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // This database is only a cache for online data, so its upgrade policy is // to simply to discard the data and start over db.execSQL(SQL_DELETE_ENTRIES); onCreate(db); } public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } }
Para acessar seu banco de dados, instancie a subclasse de SQLiteOpenHelper
:
Kotlin
val dbHelper = FeedReaderDbHelper(context)
Java
FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
Colocar informações no banco de dados
Insira dados no banco de dados passando um objeto ContentValues
para o método insert()
:
Kotlin
// Gets the data repository in write mode val db = dbHelper.writableDatabase // Create a new map of values, where column names are the keys val values = ContentValues().apply { put(FeedEntry.COLUMN_NAME_TITLE, title) put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle) } // Insert the new row, returning the primary key value of the new row val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)
Java
// Gets the data repository in write mode SQLiteDatabase db = dbHelper.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); values.put(FeedEntry.COLUMN_NAME_TITLE, title); values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle); // Insert the new row, returning the primary key value of the new row long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
O primeiro argumento de insert()
é simplesmente o nome da tabela.
O segundo argumento diz ao framework o que fazer em casos em que os ContentValues
estão vazios (ou seja, você não put
nenhum valor).
Se você especificar o nome de uma coluna, o framework incluirá uma linha e definirá o valor dessa coluna como nula. Se você especificar null
, como nesse exemplo de código, o framework não incluirá uma linha quando não houver valores.
Os métodos insert()
retornam o ID da linha recém-criada ou, caso haja um erro ao inserir os dados, retornarão -1. Isso pode acontecer caso haja um conflito com os dados preexistentes no banco de dados.
Ler informações do banco de dados
Para ler informações de um banco de dados, use o método query()
, transmitindo os seus critérios de seleção e as colunas desejadas.
O método combina elementos de insert()
e update()
, mas a lista de colunas define os dados a serem buscados (a "projeção"), em vez dos dados a serem inseridos. Os resultados da consulta são retornados em um objeto Cursor
.
Kotlin
val db = dbHelper.readableDatabase // Define a projection that specifies which columns from the database // you will actually use after this query. val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE) // Filter results WHERE "title" = 'My Title' val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?" val selectionArgs = arrayOf("My Title") // How you want the results sorted in the resulting Cursor val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC" val cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order )
Java
SQLiteDatabase db = dbHelper.getReadableDatabase(); // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE }; // Filter results WHERE "title" = 'My Title' String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?"; String[] selectionArgs = { "My Title" }; // How you want the results sorted in the resulting Cursor String sortOrder = FeedEntry.COLUMN_NAME_SUBTITLE + " DESC"; Cursor cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order );
O terceiro e o quarto argumentos (selection
e selectionArgs
) são combinados para criar uma cláusula WHERE. Como os argumentos são fornecidos separados da consulta de seleção, eles se perdem antes de serem combinados. Isso torna suas declarações de seleção imunes à injeção de SQL. Para ver mais detalhes sobre todos os argumentos, consulte a referência query()
.
Para ver uma linha no cursor, use um dos métodos de movimento do Cursor
, que sempre precisam ser chamados antes de iniciar a leitura de valores. Como o cursor começa na posição -1, ao chamar moveToNext()
, a "posição de leitura" é colocada na primeira entrada nos resultados e retorna se o cursor já passou da última entrada no conjunto de resultados. Para cada linha, você pode ler o valor de uma coluna chamando um dos métodos get Cursor
, como getString()
ou getLong()
. Para cada um dos métodos get, passe a posição de índice da coluna desejada, que pode ser vista chamando getColumnIndex()
ou getColumnIndexOrThrow()
. Quando a iteração dos resultados for concluída, chame close()
no cursor para liberar seus recursos.
O exemplo a seguir mostra como conseguir todos os IDs de itens armazenados em um cursor e adicioná-los a uma lista:
Kotlin
val itemIds = mutableListOf<Long>() with(cursor) { while (moveToNext()) { val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID)) itemIds.add(itemId) } } cursor.close()
Java
List itemIds = new ArrayList<>(); while(cursor.moveToNext()) { long itemId = cursor.getLong( cursor.getColumnIndexOrThrow(FeedEntry._ID)); itemIds.add(itemId); } cursor.close();
Excluir informações do banco de dados
Para excluir linhas de uma tabela, é necessário fornecer critérios de seleção que identifiquem as linhas ao método delete()
. O mecanismo funciona da mesma forma que os argumentos seleção para o método query()
. Ele divide a especificação de seleção em uma cláusula de seleção e em argumentos de seleção. A cláusula define as colunas a serem verificadas e permite combinar testes de coluna. Os argumentos são valores para testes comparativos que são vinculados à cláusula.
Como o resultado não é processado da mesma forma que uma instrução SQL comum, ele é imune à injeção de SQL.
Kotlin
// Define 'where' part of query. val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?" // Specify arguments in placeholder order. val selectionArgs = arrayOf("MyTitle") // Issue SQL statement. val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)
Java
// Define 'where' part of query. String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?"; // Specify arguments in placeholder order. String[] selectionArgs = { "MyTitle" }; // Issue SQL statement. int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
O valor de retorno para o método delete()
indica o número de linhas que foram excluídas do banco de dados.
Atualizar o banco de dados
Quando precisar modificar um subconjunto dos valores do banco de dados, use o método update()
.
Atualizar a tabela combina a sintaxe de ContentValues
de insert()
com a sintaxe WHERE
de delete()
.
Kotlin
val db = dbHelper.writableDatabase // New value for one column val title = "MyNewTitle" val values = ContentValues().apply { put(FeedEntry.COLUMN_NAME_TITLE, title) } // Which row to update, based on the title val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?" val selectionArgs = arrayOf("MyOldTitle") val count = db.update( FeedEntry.TABLE_NAME, values, selection, selectionArgs)
Java
SQLiteDatabase db = dbHelper.getWritableDatabase(); // New value for one column String title = "MyNewTitle"; ContentValues values = new ContentValues(); values.put(FeedEntry.COLUMN_NAME_TITLE, title); // Which row to update, based on the title String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?"; String[] selectionArgs = { "MyOldTitle" }; int count = db.update( FeedReaderDbHelper.FeedEntry.TABLE_NAME, values, selection, selectionArgs);
O valor de retorno do método update()
é o número de linhas afetadas no banco de dados.
Conexão persistente do banco de dados
Visto que é caro chamar getWritableDatabase()
e getReadableDatabase()
quando o banco de dados está fechado, deixe a conexão do banco de dados aberta durante todo o período de tempo em que possivelmente será necessário acessá-lo. Normalmente, é ideal fechar o banco de dados no onDestroy()
da atividade de chamada.
Kotlin
override fun onDestroy() { dbHelper.close() super.onDestroy() }
Java
@Override protected void onDestroy() { dbHelper.close(); super.onDestroy(); }
Depurar o banco de dados
O Android SDK inclui uma ferramenta do shell sqlite3
que permite navegar
pelo conteúdo de tabelas, executar comandos SQL e outras funções úteis em bancos de dados
SQLite. Para ver mais informações, consulte como emitir comandos do shell.