Skip to main content

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
Note: Always define DATABASE_NAME and DATABASE_VERSION as companion objects for easy access.
Warning: Never call 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)
);
Note: In Room, schema definitions are done via annotations on Kotlin classes and DAOs rather than raw SQL.

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()
Note: Always close cursors to prevent memory leaks.

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
Note: Always close the cursor after use to prevent memory leaks.

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
Warning: Using 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
Note: If 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"
    }
}
Note: Always test migrations on a copy of the database to ensure data integrity.

Migration Best Practices

  • Use AutoMigration for 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 Single-Column Index

CREATE INDEX idx_users_email ON users (email);

Example: Creating a Composite Index

CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);
Note: Indexes improve SELECT performance but slow down INSERT, UPDATE, and DELETE operations because the index must also be updated.

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
Note: Always ensure that close() is called in a finally block or using Kotlin's use extension to guarantee cleanup even if an exception occurs.

Was this article helpful?