Working with SQLite Database
1. Creating Database Helper
Use the SQLiteOpenHelper class to manage database creation and version management.
Example: Database Helper Class
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "MyApp.db"
private const val DATABASE_VERSION = 1
}
override fun onCreate(db: SQLiteDatabase) {
val CREATE_TABLE = """
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
""".trimIndent()
db.execSQL(CREATE_TABLE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT")
}
}
}
| Method | Description | Parameters |
|---|---|---|
| onCreate() | Called when the database is created for the first time. | db: SQLiteDatabase |
| onUpgrade() | Called when the database needs to be upgraded. | db: SQLiteDatabase, oldVersion: Int, newVersion: Int |
| onOpen() | Called when the database has been opened. | db: SQLiteDatabase |
DATABASE_NAME and DATABASE_VERSION as companion objects for easy access.getWritableDatabase() or getReadableDatabase() in onCreate or onUpgrade, as it may cause a recursive loop.2. Defining Database Schema
When working with SQLite in Android using Kotlin, defining the database schema involves creating tables and their relationships. The schema is typically defined using SQL statements within a SQLiteOpenHelper or through Room database.
| Component | Description | Example |
|---|---|---|
| Table Name | Name of the table to be created | users |
| Columns | Fields in the table with data types and constraints | id INTEGER PRIMARY KEY, name TEXT NOT NULL |
| Constraints | Rules for data integrity (e.g., PRIMARY KEY, NOT NULL) | UNIQUE(email), FOREIGN KEY(user_id) REFERENCES users(id) |
Example: Creating a Table in SQLite
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Example: Adding a Foreign Key Constraint
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_name TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);
3. Performing CRUD Operations
Example: Inserting Data into SQLite Table
val db = dbHelper.writableDatabase
val values = ContentValues().apply {
put("name", "John Doe")
put("email", "john@example.com")
put("age", 30)
}
val newRowId = db.insert("users", null, values)
| Operation | Description | Method | Return Value |
|---|---|---|---|
| Create | Insert new record into table | insert() |
Row ID or -1 if failed |
| Read | Retrieve records from table | query() |
Cursor object |
| Update | Modify existing records | update() |
Number of affected rows |
| Delete | Remove records from table | delete() |
Number of affected rows |
Example: Querying Data from SQLite Table
val db = dbHelper.readableDatabase
val cursor = db.query(
"users",
arrayOf("id", "name", "email"),
null,
null,
null,
null,
"name ASC"
)
while (cursor.moveToNext()) {
val id = cursor.getInt(0)
val name = cursor.getString(1)
val email = cursor.getString(2)
Log.d("User", "ID: $id, Name: $name, Email: $email")
}
cursor.close()
Example: Updating Data in SQLite Table
val db = dbHelper.writableDatabase
val values = ContentValues().apply {
put("email", "john.doe@example.com")
}
val rowsAffected = db.update(
"users",
values,
"id = ?",
arrayOf("1")
)
Example: Deleting Data from SQLite Table
val db = dbHelper.writableDatabase
val rowsDeleted = db.delete(
"users",
"id = ?",
arrayOf("1")
)
4. Using ContentValues
ContentValues is a utility class in Android used to store a set of values that are used to insert or update data in a database. It provides a clean and efficient way to manage data before inserting it into the SQLite database.
| Property | Description | Usage |
|---|---|---|
| put(String key, String value) | Stores a string value for the specified key. | contentValues.put("name", "John"); |
| put(String key, int value) | Stores an integer value for the specified key. | contentValues.put("age", 25); |
| put(String key, long value) | Stores a long value for the specified key. | contentValues.put("timestamp", System.currentTimeMillis()); |
| put(String key, double value) | Stores a double value for the specified key. | contentValues.put("salary", 50000.0); |
| put(String key, byte[] value) | Stores a byte array for the specified key. | contentValues.put("photo", photoBytes); |
| putNull(String key) | Stores a null value for the specified key. | contentValues.putNull("email"); |
Example: Inserting Data Using ContentValues
val contentValues = ContentValues().apply {
put("name", "Alice")
put("age", 30)
put("salary", 60000.0)
}
val db = dbHelper.writableDatabase
val newRowId = db.insert("employees", null, contentValues)
Example: Updating Data Using ContentValues
val contentValues = ContentValues().apply {
put("salary", 65000.0)
}
val db = dbHelper.writableDatabase
val rowsAffected = db.update(
"employees",
contentValues,
"name = ?",
arrayOf("Alice")
)
5. Querying Database
Example: Basic SELECT query
val db = helper.readableDatabase
val cursor = db.query(
"users",
arrayOf("id", "name", "email"),
null,
null,
null,
null,
"name ASC"
)
while (cursor.moveToNext()) {
val id = cursor.getInt(0)
val name = cursor.getString(1)
val email = cursor.getString(2)
println("User: $name, Email: $email")
}
cursor.close()
| Method | Description | Parameters |
|---|---|---|
| query() | Executes a SELECT query | table, columns, selection, selectionArgs, groupBy, having, orderBy |
| rawQuery() | Executes raw SQL query | sql, selectionArgs |
| rawQueryWithFactory() | Executes raw SQL with custom cursor factory | factory, sql, selectionArgs |
Example: Using rawQuery with WHERE clause
val db = helper.readableDatabase
val sql = "SELECT * FROM users WHERE age > ? AND city = ?"
val cursor = db.rawQuery(sql, arrayOf("25", "New York"))
while (cursor.moveToNext()) {
val name = cursor.getString(cursor.getColumnIndex("name"))
println("Name: $name")
}
cursor.close()
Example: COUNT query
val db = helper.readableDatabase
val sql = "SELECT COUNT(*) FROM users WHERE email LIKE ?"
val cursor = db.rawQuery(sql, arrayOf("%.com"))
if (cursor.moveToFirst()) {
val count = cursor.getInt(0)
println("Total users with .com email: $count")
}
cursor.close()
6. Working with Cursor
Example: Querying Data and Iterating with Cursor
val cursor: Cursor = db.query(
"users",
arrayOf("id", "name", "email"),
null,
null,
null,
null,
"name ASC"
)
while (cursor.moveToNext()) {
val id = cursor.getInt(cursor.getColumnIndex("id"))
val name = cursor.getString(cursor.getColumnIndex("name"))
val email = cursor.getString(cursor.getColumnIndex("email"))
println("User: $id, $name, $email")
}
cursor.close()
| Method | Description | Return Type |
|---|---|---|
| moveToFirst() | Moves cursor to the first row | Boolean |
| moveToLast() | Moves cursor to the last row | Boolean |
| moveToNext() | Moves cursor to the next row | Boolean |
| moveToPrevious() | Moves cursor to the previous row | Boolean |
| isFirst() | Checks if cursor is at the first row | Boolean |
| isLast() | Checks if cursor is at the last row | Boolean |
| isClosed() | Checks if cursor is closed | Boolean |
| getCount() | Returns total number of rows | Int |
Example: Getting Column Index
val columnIndex = cursor.getColumnIndex("name")
val name = cursor.getString(columnIndex)
| Method | Description | Return Type |
|---|---|---|
| getInt() | Gets integer value from column | Int |
| getLong() | Gets long value from column | Long |
| getString() | Gets string value from column | String |
| getDouble() | Gets double value from column | Double |
| getFloat() | Gets float value from column | Float |
| getBlob() | Gets blob value from column | ByteArray |
getColumnIndex() repeatedly is inefficient. Cache the index for reuse.Example: Using getColumnIndexOrThrow()
val nameIndex = cursor.getColumnIndexOrThrow("name")
val emailIndex = cursor.getColumnIndexOrThrow("email")
val name = cursor.getString(nameIndex)
val email = cursor.getString(emailIndex)
7. Using Transactions
Example: Basic Transaction
val db = databaseHelper.writableDatabase
db.beginTransaction()
try {
val insert1 = ContentValues().apply {
put("name", "John")
put("age", 30)
}
db.insert("users", null, insert1)
val insert2 = ContentValues().apply {
put("name", "Jane")
put("age", 25)
}
db.insert("users", null, insert2)
db.setTransactionSuccessful()
} catch (e: Exception) {
// Handle exception
} finally {
db.endTransaction()
}
| Method | Description | Notes |
|---|---|---|
| beginTransaction() | Starts a new transaction | Must be called before any operations |
| setTransactionSuccessful() | Marks the transaction as successful | Must be called before endTransaction() |
| endTransaction() | Commits or rolls back the transaction | Always called in a finally block |
setTransactionSuccessful() is not called, the transaction will be rolled back automatically.Example: Nested Transaction
val db = databaseHelper.writableDatabase
db.beginTransaction()
try {
// Insert user
val userValues = ContentValues().apply {
put("name", "Alice")
put("email", "alice@example.com")
}
val userId = db.insert("users", null, userValues)
// Insert address within same transaction
val addressValues = ContentValues().apply {
put("userId", userId)
put("street", "123 Main St")
put("city", "Anytown")
}
db.insert("addresses", null, addressValues)
db.setTransactionSuccessful()
} catch (e: Exception) {
// Handle exception
} finally {
db.endTransaction()
}
8. Implementing Database Migrations
Database migrations are essential for evolving your app's schema over time without losing existing data. In Android, Room provides a robust way to handle migrations using the Migration class.
| Migration Type | Description | Usage |
|---|---|---|
| Auto Migration | Automatically handles schema changes for simple cases (e.g., adding columns, renaming tables). | Use @AutoMigration annotation in RoomDatabase. |
| Custom Migration | Manually define schema changes for complex migrations. | Implement Migration class with SQL statements. |
Example: Auto Migration
@Database(
entities = [User::class],
version = 2,
autoMigrations = [
AutoMigration(
from = 1,
to = 2
)
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Example: Custom Migration
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user ADD COLUMN last_login INTEGER")
}
}
@Database(
entities = [User::class],
version = 2,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
val DATABASE_NAME = "app_database"
}
}
Migration Best Practices
- Use
AutoMigrationfor simple schema changes. - Define custom migrations for complex schema changes or data transformations.
- Ensure backward compatibility when defining new versions.
- Test migrations thoroughly before deploying to production.
9. Creating Indexes
Indexes in SQLite improve query performance by allowing faster data retrieval. They are created on one or more columns of a table.
| Property | Description | Notes |
|---|---|---|
| CREATE INDEX | SQL statement to create an index | Must be executed before any queries that benefit from the index |
| index_name | Name of the index to be created | Must be unique within the database |
| table_name | Name of the table to create index on | Must exist in the database |
| column_list | Comma-separated list of columns to index | Can include multiple columns for composite indexes |
Example: Creating a Composite Index
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);
10. Closing Database Connections
| Method | Description | Notes |
|---|---|---|
| close() | Closes the database connection and releases resources. | Must be called to prevent memory leaks. Not calling it may lead to database locks or file descriptor exhaustion. |
Example: Closing Database Connection
val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.CREATE_IF_NECESSARY)
// Perform database operations
db.close() // Closes the database connection
close() is called in a finally block or using Kotlin's use extension to guarantee cleanup even if an exception occurs.