Как работать с SQLite в Android Studio

Для работы с SQLite в Android Studio создайте локальную базу в приложении через SQLiteOpenHelper. SQLite хранит данные без отдельного сервера. В этой инструкции вы создадите две связанные таблицы, выполните CRUD и JOIN, покажете Cursor в RecyclerView и Spinner, а затем проверите записи в Android Studio.

Что такое SQLite в Android

SQLite – встраиваемая реляционная база данных. Приложение хранит ее в файле и обращается к данным через SQL или Android API.

SQLite использует 5 классов хранения: NULL, INTEGER, REAL, TEXT и BLOB. INTEGER подходит для целых чисел и временных меток, TEXT – для строк, REAL – для дробных чисел, а BLOB – для двоичных данных.

Android предлагает два основных пути работы с SQLite. SQLiteOpenHelper управляет созданием файла и версиями схемы. Room добавляет проверку SQL-запросов и работу с объектами.

Схема работы встроенной базы SQLite в Android-приложении

Подготовка проекта Android

Подключите AndroidX-библиотеки и включите привязку представлений. Support Library и конфигурация compile не подходят для актуального проекта.

android {
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation("androidx.recyclerview:recyclerview:$recyclerviewVersion")
    implementation("androidx.cardview:cardview:$cardviewVersion")
    implementation("com.google.android.material:material:$materialVersion")
}

Задайте версии библиотек в version catalog или переменных Gradle проекта. Номера AndroidX берите из каталога версий AndroidX. Для Material используйте версию, согласованную с зависимостями проекта.

Запросы к базе нельзя выполнять в главном потоке. Выносите чтение и запись в Executor или корутины, чтобы дисковая операция не блокировала интерфейс.

private final ExecutorService databaseExecutor = Executors.newSingleThreadExecutor();

private void loadEmployers() {
    databaseExecutor.execute(() -> {
        try (Cursor cursor = helper.getReadableDatabase().query(
                SampleDBContract.Employer.TABLE_NAME,
                new String[]{SampleDBContract.Employer._ID,
                        SampleDBContract.Employer.COLUMN_NAME},
                null, null, null, null, null)) {
            ArrayList<String> names = new ArrayList<>();
            while (cursor.moveToNext()) {
                names.add(cursor.getString(cursor.getColumnIndexOrThrow(
                        SampleDBContract.Employer.COLUMN_NAME)));
            }
            runOnUiThread(() -> showEmployerNames(names));
        }
    });
}

Здесь showEmployerNames() обозначает ваш метод обновления интерфейса. Доступ к базе выполняется в фоновом потоке, а результат передается в UI.

Структура таблиц и контракта

Пример использует 2 таблицы. Employer хранит работодателей, а Employee – сотрудников и ссылку на работодателя через employer_id.

Главный экран Android-приложения с переходами к таблицам Employer и Employee

Описывайте имена таблиц и столбцов в отдельном контракте. Такой класс уменьшает число опечаток в SQL и помогает использовать одинаковые имена в запросах и адаптерах.

public final class SampleDBContract {
    private SampleDBContract() {}

    public static class Employer implements BaseColumns {
        public static final String TABLE_NAME = "employer";
        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_DESCRIPTION = "description";
        public static final String COLUMN_FOUNDED_DATE = "founded_date";

        public static final String CREATE_TABLE =
                "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_NAME + " TEXT NOT NULL, " +
                COLUMN_DESCRIPTION + " TEXT, " +
                COLUMN_FOUNDED_DATE + " INTEGER)";
    }

    public static class Employee implements BaseColumns {
        public static final String TABLE_NAME = "employee";
        public static final String COLUMN_FIRSTNAME = "firstname";
        public static final String COLUMN_LASTNAME = "lastname";
        public static final String COLUMN_EMPLOYER_ID = "employer_id";

        public static final String CREATE_TABLE =
                "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_FIRSTNAME + " TEXT NOT NULL, " +
                COLUMN_LASTNAME + " TEXT NOT NULL, " +
                COLUMN_EMPLOYER_ID + " INTEGER NOT NULL, " +
                "FOREIGN KEY(" + COLUMN_EMPLOYER_ID + ") REFERENCES " +
                Employer.TABLE_NAME + "(" + Employer._ID + "))";
    }
}

Добавьте в файл контракта импорт android.provider.BaseColumns. В этом примере дата хранится как INTEGER в миллисекундах. Форматируйте дату только при выводе.

Создание базы через SQLiteOpenHelper

SQLiteOpenHelper вызывает onCreate() для нового файла и onUpgrade() при увеличении номера схемы. Создайте один helper на время жизни приложения, а не новый объект для каждого запроса.

public class SampleDBSQLiteHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 2;
    public static final String DATABASE_NAME = "sample_database";
    private static SampleDBSQLiteHelper instance;

    public static synchronized SampleDBSQLiteHelper getInstance(Context context) {
        if (instance == null) {
            instance = new SampleDBSQLiteHelper(context);
        }
        return instance;
    }

    private SampleDBSQLiteHelper(Context context) {
        super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);
        db.setForeignKeyConstraintsEnabled(true);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SampleDBContract.Employer.CREATE_TABLE);
        db.execSQL(SampleDBContract.Employee.CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            db.execSQL(SampleDBContract.Employee.CREATE_TABLE);
        }
    }
}

Для helper добавьте импорты android.content.Context, android.database.sqlite.SQLiteDatabase и android.database.sqlite.SQLiteOpenHelper. При переходе с версии 1 на 2 oldVersion равен 1, поэтому новая таблица добавляется без удаления Employer.

Экран добавления работодателя в SQLite-приложении
Форма работы с таблицей работодателей в Android-приложении
private SampleDBSQLiteHelper helper;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    helper = SampleDBSQLiteHelper.getInstance(this);
}

Общий helper не закрывайте при уничтожении отдельной Activity. Другие экраны могут продолжать работу с той же базой. Каждый экран закрывает собственные Cursor и завершает собственный Executor.

CRUD: вставка, выборка, обновление и удаление

Операции CRUD работают через ContentValues и методы SQLiteDatabase. Один объект ContentValues описывает строку, которую вы добавляете или изменяете.

Вставка строки

Передайте значения в ContentValues и вызовите insert(). Метод вернет идентификатор новой строки или значение меньше нуля при ошибке.

private long saveEmployer(String name, String description, long foundedDate) {
    ContentValues values = new ContentValues();
    values.put(SampleDBContract.Employer.COLUMN_NAME, name);
    values.put(SampleDBContract.Employer.COLUMN_DESCRIPTION, description);
    values.put(SampleDBContract.Employer.COLUMN_FOUNDED_DATE, foundedDate);
    return helper.getWritableDatabase().insert(
            SampleDBContract.Employer.TABLE_NAME, null, values);
}

Выборка с условиями

Плейсхолдеры ? и selectionArgs отделяют пользовательские значения от SQL-команды и защищают запрос от SQL-инъекций.

private Cursor readEmployers(String name, String description, long date) {
    String selection = SampleDBContract.Employer.COLUMN_NAME + " LIKE ? AND " +
            SampleDBContract.Employer.COLUMN_FOUNDED_DATE + " > ? AND " +
            SampleDBContract.Employer.COLUMN_DESCRIPTION + " LIKE ?";
    String[] args = {"%" + name + "%", String.valueOf(date),
            "%" + description + "%"};

    return helper.getReadableDatabase().query(
            SampleDBContract.Employer.TABLE_NAME,
            new String[]{SampleDBContract.Employer._ID,
                    SampleDBContract.Employer.COLUMN_NAME,
                    SampleDBContract.Employer.COLUMN_DESCRIPTION,
                    SampleDBContract.Employer.COLUMN_FOUNDED_DATE},
            selection, args, null, null, null);
}

Запрос ищет подстроки одновременно в имени и описании. Дата должна быть больше переданного значения, поэтому условие использует знак >.

try {
    date = dateFormat.parse(input).getTime();
} catch (ParseException exception) {
    Log.e(TAG, "Cannot parse founded date", exception);
    return;
}

Если разбор даты завершился ошибкой, метод записывает исключение в Log и прекращает операцию. Нулевая дата не подставляется молча.

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

Для изменения и удаления передайте whereClause и whereArgs. Так операция затронет только выбранную строку.

private int updateEmployer(long id, String name, String description) {
    ContentValues values = new ContentValues();
    values.put(SampleDBContract.Employer.COLUMN_NAME, name);
    values.put(SampleDBContract.Employer.COLUMN_DESCRIPTION, description);
    return helper.getWritableDatabase().update(
            SampleDBContract.Employer.TABLE_NAME,
            values,
            SampleDBContract.Employer._ID + " = ?",
            new String[]{String.valueOf(id)});
}

private int deleteEmployer(long id) {
    return helper.getWritableDatabase().delete(
            SampleDBContract.Employer.TABLE_NAME,
            SampleDBContract.Employer._ID + " = ?",
            new String[]{String.valueOf(id)});
}

Число, которое вернул update() или delete(), показывает количество измененных строк. Значение 0 означает, что условие не нашло запись.

Внешний ключ и вставка Employee

Внешний ключ связывает Employee с существующей строкой Employer. SQLite по умолчанию не включает проверку FOREIGN KEY, поэтому helper вызывает setForeignKeyConstraintsEnabled(true) в onConfigure().

@Override
public void onConfigure(SQLiteDatabase db) {
    super.onConfigure(db);
    db.setForeignKeyConstraintsEnabled(true);
}

Получите идентификатор выбранного работодателя из Cursor Spinner и передайте его в полный метод сохранения сотрудника.

private long saveEmployee(String firstname, String lastname, long employerId) {
    ContentValues values = new ContentValues();
    values.put(SampleDBContract.Employee.COLUMN_FIRSTNAME, firstname);
    values.put(SampleDBContract.Employee.COLUMN_LASTNAME, lastname);
    values.put(SampleDBContract.Employee.COLUMN_EMPLOYER_ID, employerId);

    return helper.getWritableDatabase().insertOrThrow(
            SampleDBContract.Employee.TABLE_NAME, null, values);
}

Передайте в saveEmployee() идентификатор выбранного работодателя и введенные имя и фамилию. Если идентификатор не существует, включенное ограничение внешнего ключа отклонит вставку.

Форма создания сотрудника с выбором работодателя

Вывод Cursor в RecyclerView

Cursor хранит строки результата и позволяет перейти к нужной позиции через moveToPosition(). Адаптер закрывает старый Cursor перед заменой новым.

public class EmployerCursorAdapter
        extends RecyclerView.Adapter<EmployerCursorAdapter.ViewHolder> {
    private Cursor cursor;

    public EmployerCursorAdapter(Cursor cursor) {
        this.cursor = cursor;
    }

    public void replaceCursor(Cursor nextCursor) {
        if (cursor == nextCursor) {
            return;
        }
        if (cursor != null) {
            cursor.close();
        }
        cursor = nextCursor;
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        return cursor == null ? 0 : cursor.getCount();
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        cursor.moveToPosition(position);
        holder.bindCursor(cursor);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.employer_list_item, parent, false);
        return new ViewHolder(view);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        ViewHolder(View itemView) {
            super(itemView);
        }

        void bindCursor(Cursor cursor) {
            TextView name = itemView.findViewById(R.id.nameLabel);
            TextView description = itemView.findViewById(R.id.descLabel);
            name.setText(cursor.getString(cursor.getColumnIndexOrThrow(
                    SampleDBContract.Employer.COLUMN_NAME)));
            description.setText(cursor.getString(cursor.getColumnIndexOrThrow(
                    SampleDBContract.Employer.COLUMN_DESCRIPTION)));
        }
    }
}

Создайте файл res/layout/employer_list_item.xml с двумя текстовыми полями.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/nameLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/descLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Используйте импорты Cursor, LayoutInflater, View, ViewGroup и TextView из Android SDK. RecyclerView импортируйте из androidx.recyclerview.widget.RecyclerView. При уничтожении экрана вызовите adapter.replaceCursor(null).

Список работодателей из Cursor в RecyclerView

Вывод данных SQLite в Spinner

SimpleCursorAdapter ожидает Cursor с обязательным столбцом _id. Получите его вместе с названием работодателя и закройте Cursor, когда Spinner перестанет им пользоваться.

Создайте адаптер в главном потоке с пустым Cursor. Чтение базы выполните через общий helper в фоне.

private SimpleCursorAdapter spinnerAdapter;
private boolean screenDestroyed;

private void configureSpinner() {
    spinnerAdapter = new SimpleCursorAdapter(
            this,
            android.R.layout.simple_spinner_item,
            null,
            new String[]{SampleDBContract.Employer.COLUMN_NAME},
            new int[]{android.R.id.text1},
            0);
    spinnerAdapter.setDropDownViewResource(
            android.R.layout.simple_spinner_dropdown_item);
    binding.employerSpinner.setAdapter(spinnerAdapter);
}

private void loadSpinner() {
    databaseExecutor.execute(() -> {
        Cursor cursor = helper.getReadableDatabase().query(
                SampleDBContract.Employer.TABLE_NAME,
                new String[]{SampleDBContract.Employer._ID,
                        SampleDBContract.Employer.COLUMN_NAME},
                null, null, null, null,
                SampleDBContract.Employer.COLUMN_NAME + " ASC");
        cursor.getCount();
        runOnUiThread(() -> {
            if (screenDestroyed) {
                cursor.close();
            } else {
                spinnerAdapter.changeCursor(cursor);
            }
        });
    });
}

@Override
protected void onDestroy() {
    screenDestroyed = true;
    spinnerAdapter.changeCursor(null);
    databaseExecutor.shutdown();
    super.onDestroy();
}

Вызовите configureSpinner() после создания формы, затем вызовите loadSpinner(). Метод changeCursor() закрывает прежний Cursor при замене результата.

Spinner со списком работодателей из базы SQLite

JOIN-запрос через rawQuery()

JOIN объединяет сотрудников с названиями их работодателей. Метод rawQuery() принимает SQL-строку и аргументы для плейсхолдеров. Перечислите столбцы явно, чтобы два столбца _id не столкнулись в результате.

Добавьте константу внутрь SampleDBContract, вне вложенных классов Employer и Employee.

public static final String SELECT_EMPLOYEE_WITH_EMPLOYER =
        "SELECT ee._id AS employee_id, " +
        "ee.firstname, ee.lastname, " +
        "er._id AS employer_id, er.name AS employer_name " +
        "FROM employee ee " +
        "INNER JOIN employer er " +
        "ON ee.employer_id = er._id " +
        "WHERE ee.firstname LIKE ? AND ee.lastname LIKE ?";

Разметка строки и адаптер результата

Создайте файл res/layout/employee_list_item.xml с полями employeeLabel и employerLabel. Адаптер получает Cursor, перемещает его к позиции строки и закрывает старый результат при замене.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/employeeLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/employerLabel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
public class JoinCursorAdapter
        extends RecyclerView.Adapter<JoinCursorAdapter.ViewHolder> {
    private Cursor cursor;

    public JoinCursorAdapter(Cursor cursor) {
        this.cursor = cursor;
    }

    public void replaceCursor(Cursor nextCursor) {
        if (cursor == nextCursor) {
            return;
        }
        if (cursor != null) {
            cursor.close();
        }
        cursor = nextCursor;
        notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
        return cursor == null ? 0 : cursor.getCount();
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        cursor.moveToPosition(position);
        holder.bind(cursor);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.employee_list_item, parent, false);
        return new ViewHolder(view);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        ViewHolder(View itemView) {
            super(itemView);
        }

        void bind(Cursor cursor) {
            TextView employee = itemView.findViewById(R.id.employeeLabel);
            TextView employer = itemView.findViewById(R.id.employerLabel);
            String firstname = cursor.getString(cursor.getColumnIndexOrThrow("firstname"));
            String lastname = cursor.getString(cursor.getColumnIndexOrThrow("lastname"));
            String employerName = cursor.getString(
                    cursor.getColumnIndexOrThrow("employer_name"));
            employee.setText(firstname + " " + lastname);
            employer.setText(employerName);
        }
    }
}

Получение Cursor в фоне

Создайте отдельный экран EmployeeListActivity для вывода результата JOIN. Экран создаст RecyclerView программно, подключит JoinCursorAdapter и запустит чтение в Executor.

Сохраните класс в файле EmployeeListActivity.java рядом с helper и контрактом. Добавьте строку <activity android:name=".EmployeeListActivity" /> внутри элемента application в AndroidManifest.xml.

import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EmployeeListActivity extends Activity {
    private final ExecutorService databaseExecutor =
            Executors.newSingleThreadExecutor();
    private SampleDBSQLiteHelper helper;
    private JoinCursorAdapter adapter;
    private boolean screenDestroyed;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        helper = SampleDBSQLiteHelper.getInstance(this);

        RecyclerView recyclerView = new RecyclerView(this);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new JoinCursorAdapter(null);
        recyclerView.setAdapter(adapter);
        setContentView(recyclerView);

        loadEmployees("", "");
    }

    private Cursor readEmployees(String firstname, String lastname) {
        String[] args = {"%" + firstname + "%", "%" + lastname + "%"};
        return helper.getReadableDatabase().rawQuery(
                SampleDBContract.SELECT_EMPLOYEE_WITH_EMPLOYER, args);
    }

    private void loadEmployees(String firstname, String lastname) {
        databaseExecutor.execute(() -> {
            Cursor result = readEmployees(firstname, lastname);
            try {
                result.getCount();
            } catch (RuntimeException exception) {
                result.close();
                throw exception;
            }
            runOnUiThread(() -> {
                if (screenDestroyed) {
                    result.close();
                } else {
                    adapter.replaceCursor(result);
                }
            });
        });
    }

    @Override
    protected void onDestroy() {
        screenDestroyed = true;
        adapter.replaceCursor(null);
        databaseExecutor.shutdown();
        super.onDestroy();
    }
}

Добавьте в начало Java-файла строку package с пакетом вашего приложения. Для JoinCursorAdapter используйте те же импорты представлений, что и для EmployerCursorAdapter.

Откройте экран из существующей Activity вызовом startActivity(new android.content.Intent(this, EmployeeListActivity.class));, например в обработчике кнопки. Если таблица Employee содержит записи, список покажет имена сотрудников и работодателей.

Пустые строки в loadEmployees() превращаются в шаблоны %%. Запрос выбирает всех сотрудников с существующим работодателем. После передачи результата адаптер владеет Cursor и закрывает его при замене.

При уничтожении экрана onDestroy() закрывает текущий Cursor и завершает Executor. Если фоновый запрос завершится позже, обработчик результата закроет его Cursor без обновления интерфейса.

Результат JOIN-запроса сотрудников и работодателей

Миграция схемы без потери данных

Версия базы растет вместе со схемой. При переходе с версии 1 на 2 SQLite передаст oldVersion == 1, поэтому проверяйте диапазон через сравнение с целевой версией.

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion < 2) {
        db.execSQL(SampleDBContract.Employee.CREATE_TABLE);
    }
}

Этот переход предполагает, что версия 1 уже содержит Employer с теми же столбцами. Переименование столбца требует отдельной миграции.

Не удаляйте таблицы в onUpgrade(), если задача не требует уничтожить данные. Для сложного изменения создайте новую таблицу, перенесите строки и удалите старую после успешного переноса.

Проверка базы в Android Studio

Для просмотра записей SQLite в Android Studio используйте Database Inspector. Он показывает базы запущенного приложения, таблицы и результаты SQL-запросов.

  1. Запустите приложение на устройстве или эмуляторе с API 26 или выше.
  2. В Android Studio откройте «View» → «Tool Windows» → «App Inspection».
  3. Выберите вкладку «Database Inspector».
  4. В меню выберите процесс запущенного приложения. Базы появятся в панели «Databases».
  5. Раскройте узел sample_database и дважды щелкните имя таблицы. В окне появятся ее строки.
  6. Нажмите «Open New Query tab», введите SELECT * FROM employer и выполните запрос.

После вставки появится строка с новым _id. Если база не отображается, вызовите getWritableDatabase() в приложении и подключите Database Inspector к запущенному процессу снова.

SQLiteOpenHelper или Room

SQLiteOpenHelper подходит для небольшой базы, учебного проекта и ситуации, где вам нужен прямой контроль над SQL. Room удобнее в приложении с большим числом запросов: он связывает таблицы с Entity, проверяет SQL при сборке и организует доступ через Dao.

Google рекомендует использовать Room вместо прямой работы с SQLite API. Подключите библиотеку Room с версиями, согласованными с проектом.

dependencies {
    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")
}
@Entity
public class EmployerEntity {
    @PrimaryKey(autoGenerate = true)
    public long id;
    public String name;
}

@Dao
public interface EmployerDao {
    @Insert
    long insert(EmployerEntity employer);

    @Query("SELECT * FROM EmployerEntity ORDER BY name")
    List<EmployerEntity> findAll();
}

@Database(entities = {EmployerEntity.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract EmployerDao employerDao();
}

Это отдельная схема для знакомства с Entity, Dao и Database. Она не заменяет существующую базу примера без настройки таблиц и миграций.

Выбирайте Room, когда схема растет и запросы должны проходить проверку при сборке. Оставляйте SQLiteOpenHelper, когда прямой SQL важнее слоя абстракции и база остается небольшой.

Как организовать работу с SQLite в Android-приложении

SQLiteOpenHelper помогает создать локальную базу, а ContentValues и Cursor закрывают базовые операции CRUD. Для связей включайте внешние ключи, для миграций повышайте версию без DROP, а для JOIN перечисляйте столбцы и используйте rawQuery().

После первого рабочего CRUD-сценария добавьте тест перехода с версии 1 на 2. Сохраните работодателя до обновления и убедитесь, что запись осталась после миграции.

Совет от нашей редакции: держите один helper, закрывайте каждый замененный Cursor и выполняйте операции с базой за пределами главного потока.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев