SQLite в Android на Java: создание базы, таблиц и выполнение запросов

Кратко: используйте SQLiteOpenHelper для создания и обновления БД, в onCreate выполняйте DDL через db.execSQL(), а для операций CRUD — db.query/rawQuery и методы insert/update/delete с ContentValues; обязательно закрывайте Cursor и БД.

Подготовка и DatabaseHelper

  1. Разрешения: для внутренней БД дополнительных разрешений не требуется (WRITE_EXTERNAL_STORAGE не нужен для стандартной локальной БД с API 19+).
  2. Создайте класс, наследующий SQLiteOpenHelper и инициализируйте его в Activity/Service.

Пример DatabaseHelper:

public class DatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "myapp.db";
    private static final int DATABASE_VERSION = 1;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // создание таблиц см. ниже
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // простая стратегия: пересоздать таблицы (для продакшна миграции лучше писать отдельно)
        db.execSQL("DROP TABLE IF EXISTS users");
        onCreate(db);
    }
}

Инициализация:

DatabaseHelper dbHelper = new DatabaseHelper(this);

Создание таблиц и индексы

В onCreate используйте db.execSQL() с DDL. Пример таблицы users:

@Override
public void onCreate(SQLiteDatabase db) {
    String create = "CREATE TABLE users (" +
        "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
        "name TEXT NOT NULL, " +
        "email TEXT UNIQUE, " +
        "age INTEGER DEFAULT 18)";
    db.execSQL(create);
    db.execSQL("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)");
}

Пояснения:

  • PRIMARY KEY AUTOINCREMENT — даёт уникальный id.
  • NOT NULL — для обязательных полей.
  • UNIQUE — предотвращает дублирование (например, email).
  • Индексы ускоряют выборки по колонкам с фильтрами/соединениями.

Добавляйте NOT NULL для обязательных полей и индексы по часто используемым колонкам. Используйте IF NOT EXISTS при создании индексов.

CRUD: SELECT, INSERT, UPDATE, DELETE и транзакции

Чтение (безопасно):

SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor c = db.query("users",
    new String[]{"id","name","email","age"},
    "age > ?", new String[]{"25"},
    null, null, "name ASC");
if (c.moveToFirst()) {
    do {
        int id = c.getInt(0);
        String name = c.getString(1);
        // обработка
    } while (c.moveToNext());
}
c.close();
db.close();

Сырой запрос (rawQuery) — для JOIN, GROUP BY и сложных выражений:

Cursor c = db.rawQuery("SELECT u.id,u.name,o.total FROM users u JOIN orders o ON u.id=o.user_id WHERE o.total>?", new String[]{"1000"});

Вставка:

SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues vals = new ContentValues();
vals.put("name","Иван Иванов");
vals.put("email","ivan@example.com");
vals.put("age",30);
long id = db.insert("users", null, vals);
db.close();

Массовая вставка — в транзакции для скорости:

db.beginTransaction();
try {
    for (...) {
        db.insert(...);
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

Обновление и удаление:

// update
ContentValues v = new ContentValues();
v.put("age", 31);
int cnt = db.update("users", v, "id = ?", new String[]{String.valueOf(1)});
// delete
int del = db.delete("users", "id = ?", new String[]{String.valueOf(1)});

Не забудьте всегда закрывать Cursor и вызов db.close() в finally-блоке или использовать try-with-resources (для API, где поддерживается). Не формируйте SQL через конкатенацию строк — используйте параметры, чтобы избежать инъекций.

Частые ошибки

  • Забывают закрыть Cursor или SQLiteDatabase → утечки и ошибки "database locked".
  • Меняют схему без корректной миграции в onUpgrade → потеря данных.
  • Конкатенация параметров в SQL вместо placeholders → риск SQL‑инъекции.
  • Отсутствие индексов для часто фильтруемых колонок → медленные запросы.

FAQ

  • Нужно ли WRITE_EXTERNAL_STORAGE для SQLite? Нет, если вы храните БД во внутреннем хранилище приложения.
  • Когда использовать rawQuery вместо db.query()? Для сложных JOIN/GROUP BY/подзапросов, db.query удобен для простых выборок.
  • Как безопасно обновлять схему БД в продакшне? Реализуйте пошаговые миграции в onUpgrade, сохраняйте резервные копии при крупных изменениях.

Этот набор приёмов покрывает большинство задач по работе с SQLite в Android на Java: создавайте DatabaseHelper, проектируйте схемы с индексами, используйте параметры запросов и транзакции для производительности.