Salvar dados usando o SQLite

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 _IDque 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.