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

В этом руководстве я подробно расскажу о том, как использовать базу данных Android SQLite.

Что такое SQLite

SQLite - это система управления реляционными базами данных, похожая на Oracle, MySQL, PostgreSQL и SQL Server. Она реализует большую часть стандарта SQL, но в отличие от четырех упомянутых выше СУБД она не поддерживает модель «клиент-сервер». Скорее, она встроена в конечную программу. Это означает, что можно связать базу данных SQLite с приложением и получить доступ ко всем возможностям БД в своем приложении.

Что такое SQLite

Данная СУБД совместима как с Android, так и с iOS, и каждое приложение может создавать и использовать базу данных SQLite. В Android контакты и медиа хранятся и ссылаются на БД SQLite. Она является наиболее используемой СУБД в мире и самым распространенным программным обеспечением. Чтобы узнать о базах данных SQLite как можно больше, посетите официальный сайт SQLite.

Подготовка

Чтобы включить привязку данных в приложении, нужно добавить в файл build.gradle следующий код:

dataBinding.enabled = true

Чтобы использовать как RecyclerView, так и CardView для отображения списков, нужно включить соответствующие библиотеки в разделе зависимостей в файле build.gradle:

dependencies {
	...
    compile 'com.android.support:design:24.2.1'
    compile 'com.android.support:cardview-v7:24.2.1'
}

Чтобы задействовать все возможности базы данных SQLite, лучше изучить синтаксис SQL.

Описание примера приложения

В нашем Android SQLite примере мы создадим две таблицы: Employer и Employee. Таблица Employee будет содержать ссылку на внешний ключ таблицы Employer. Мы рассмотрим, как вставлять, выбирать, обновлять и удалять строки из таблиц. Я также продемонстрирую, как вывести элементы, выбранные из базы данных SQLite в RecyclerView (список) и в Spinner.

У нас есть MainActivity, из которого можно перейти к EmployerActivity (для работы с таблицей Employer) или к EmployeeActivity (для работы с таблицей Employee):

Описание примера приложения

Классы хранения базы данных SQLite

Классы определяют то, как данные хранятся в базе. SQLite сохраняют значения с помощью пяти доступных классов хранения:

  • NULL - нулевое значение;
  • INTEGER - для целых чисел, содержащих от 1 до 8 байтов;
  • REAL - числа с плавающей запятой;
  • TEXT - текстовые строки, хранящиеся с использованием кодировки базы данных (UTF-8 или UTF-16);
  • BLOB - двоичные данные, хранящиеся точно так, как они были введены.

Определение таблиц

Поскольку база данных SQLite является локальной, нужно обеспечить, чтобы приложение создавало таблицы и по мере необходимости сбрасывало их.

Начнем с Android SQLite query создания таблицы Employer, а затем перейдем к EmployerActivity.

Рекомендуется размещать логику создания базы х SQLite в классе. Это облегчает устранение возможных неполадок. Назовем наш класс SampleDBContract:

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 = "date";
        public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " +
                TABLE_NAME + " (" +
                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_NAME + " TEXT, " +
                COLUMN_DESCRIPTION + " TEXT, " +
                COLUMN_FOUNDED_DATE + " INTEGER" + ")";
    }
}

Мы определяем частный конструктор для SampleDBContract, а затем создаем класс для представления таблицы Employer. Обратите внимание: класс Employer реализует интерфейс BaseColumns. Он предоставляет два столбца в нашей таблице. Это столбец _ID, который будет автоматически увеличиваться при добавлении каждой новой строки. И столбец _COUNT, который может использоваться ContentProviders для возврата количества записей, извлекаемых через запрос. Столбец _COUNT не является обязательным. Строка CREATE_TABLE компилируется в следующий оператор SQL:

CREATE TABLE IF NOT EXISTS employer (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, date INTEGER)

На данный момент в нашем Android SQLite примере мы определили схему таблицы Employer.

Создание базы данных с помощью SQLiteOpenHelper

Самый простой способ управления созданием базы данных и версиями - создать подкласс SQLiteOpenHelper. Он упрощает управление базой данных SQLite, создавая БД, если они не существуют. Необходимо только переопределить методы onCreate() и onUpgrade(), чтобы указать нужное действие для создания или обновления базы данных:

public class SampleDBSQLiteHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "sample_database";
    public SampleDBSQLiteHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(SampleDBContract.Employer.CREATE_TABLE);
    }
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + SampleDBContract.Employer.TABLE_NAME);
        onCreate(sqLiteDatabase);
    }
}

Теперь в нашем примере Android database SQLite задаем для нашей базы данных SQLite имя (sample_database). Конструктор вызывает конструктор суперкласса с именем и версией базы данных. В onCreate мы указываем объекту SQLiteDatabase выполнить оператор Employer CREATE_TABLE SQL. Через onUpgrade мы сбрасываем таблицу Employer и создаем ее снова:

Создание базы данных с помощью SQLiteOpenHelper

Таблица Employer имеет три столбца: name, description и founded_date. Нажатие кнопки сохранения вызывает метод saveToDB():

Создание базы данных с помощью SQLiteOpenHelper - 2

В saveToDB() мы получаем ссылку на объект SQLiteDatabase, используя метод getWritableDatabase() из SQLiteOpenHelper. Этот метод создает базу данных, если она еще не существует, или открывает ее, если она уже создана. GetWritableDatabase возвращает объект SQLiteDatabase, который открывает доступ на чтение / запись:

private void saveToDB() {
        SQLiteDatabase database = new SampleDBSQLiteHelper(this).getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(SampleDBContract.Employer.COLUMN_NAME, binding.nameEditText.getText().toString());
        values.put(SampleDBContract.Employer.COLUMN_DESCRIPTION, binding.descEditText.getText().toString());
        try {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime((new SimpleDateFormat("dd/MM/yyyy")).parse(
                    binding.foundedEditText.getText().toString()));
            long date = calendar.getTimeInMillis();
            values.put(SampleDBContract.Employer.COLUMN_FOUNDED_DATE, date);
        }
        catch (Exception e) {
            Log.e(TAG, "Error", e);
            Toast.makeText(this, "Date is in the wrong format", Toast.LENGTH_LONG).show();
            return;
        }
        long newRowId = database.insert(SampleDBContract.Employer.TABLE_NAME, null, values);
        Toast.makeText(this, "The new Row Id is " + newRowId, Toast.LENGTH_LONG).show();
    }

В приведенном выше фрагменте кода есть четыре момента:

  1. Мы получаем объект SQLiteDatabase, который открывает доступ на запись в базу данных;
  2. Значения, которые будут храниться в базе данных, помещаются в объект ContentValue с именем столбца в качестве ключа;
  3. Мы помещаем Date в объект ContentValue, который переводится в класс хранения данных Android SQLite INTEGER;
  4. При вставке строки в базу данных с помощью метода database.insert() возвращается идентификатор строки.

Выбор данных из базы данных SQLite

Подобно тому, как мы применили метод getWritableDatabase(), можно вызвать getReadableDatabase() объекта SQLiteOpenHelper для получения объекта SQLiteDatabase, который можно использовать для чтения информации из базы данных. Стоит отметить, что объект SQLiteDatabase, возвращаемый getReadableDatabase(), предоставляет собой тот же самый доступ на чтение / запись в базу данных, который был возвращен функцией getWritableDatabase(), за исключением тех случаев, когда существуют определенные ограничения. Например, файловая система, содержащая заполненную базу данных, и база данных может быть открыта только для чтения.

Метод readFromDB будет запрашивать БД, и возвращать все строки из таблицы Employer, в которых имя или описание из таблицы Employer совпадает со значением, введенным в EditText. А также строки, в которых дата основания компании совпадает со значением, введенным в EditText:

private void readFromDB() {
        String name = binding.nameEditText.getText().toString();
        String desc = binding.descEditText.getText().toString();
        long date = 0;
        try {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime((new SimpleDateFormat("dd/MM/yyyy")).parse(
                    binding.foundedEditText.getText().toString()));
            date = calendar.getTimeInMillis();
        }
        catch (Exception e) {}
        SQLiteDatabase database = new SampleDBSQLiteHelper(this).getReadableDatabase();
        String[] projection = {
                SampleDBContract.Employer._ID,
                SampleDBContract.Employer.COLUMN_NAME,
                SampleDBContract.Employer.COLUMN_DESCRIPTION,
                SampleDBContract.Employer.COLUMN_FOUNDED_DATE
        };
        String selection =
                SampleDBContract.Employer.COLUMN_NAME + " like ? and " +
                        SampleDBContract.Employer.COLUMN_FOUNDED_DATE + " > ? and " +
                        SampleDBContract.Employer.COLUMN_DESCRIPTION + " like ?";
        String[] selectionArgs = {"%" + name + "%", date + "", "%" + desc + "%"};
        Cursor cursor = database.query(
                SampleDBContract.Employer.TABLE_NAME,     // Запрашиваемая таблица
                projection,                               // Возвращаемый столбец
                selection,                                // Столбец для условия WHERE
                selectionArgs,                            // Значение для условия WHERE
                null,                                     // не группировать строки
                null,                                     // не фильтровать по группам строк
                null                                      // не сортировать
        );
        Log.d(TAG, "The total cursor count is " + cursor.getCount());
        binding.recycleView.setAdapter(new SampleRecyclerViewCursorAdapter(this, cursor));
    }

В коде Android SQLite query, приведенного выше, projection является массивом String, представляющим столбцы, которые мы хотим получить. selection является строковым представлением условия SQL WHERE, отформатированным таким образом, что символ '?' будет заменен аргументами в массиве selectionArgs String. Вы также можете группировать, фильтровать и сортировать результаты запроса. Вставка данных в базу SQLite с использованием описанного выше метода защищает от SQL-инъекций.

Обратите внимание на объект, возвращаемый запросом - Cursor. В следующем разделе мы покажем, как вывести содержимое Cursor с помощью RecyclerView.

Отображение содержимого объекта Cursor в RecyclerView

Cursor предоставляет произвольный доступ к набору результатов, возвращаемому запросом к базе данных. Это означает, что через Cursor можно получить доступ к значениям в любом месте, подобно Java-спискам или массивам. Благодаря этому приему можно реализовать RecyclerView с использованием Cursor так же, как мы реализуем RecyclerView с помощью ArrayLists. Вместо вызова List.get(i), вы перемещаете Cursor в нужную позицию, используя moveToPosition(). После этого вызываете соответствующий метод getXXX(int columnIndex), где XXX - это Blob, Double, Float, Int, Long, Short или String.

Чтобы не беспокоиться о корректных индексах столбцов из метода readFromDB(), примененного выше, мы используем метод getColumnIndexOrThrow(), который извлекает индекс указанного столбца или генерирует исключение, если имя столбца не существует внутри объекта Cursor:

public class SampleRecyclerViewCursorAdapter extends RecyclerView.Adapter<SampleRecyclerViewCursorAdapter.ViewHolder> {
    Context mContext;
    Cursor mCursor;
    public SampleRecyclerViewCursorAdapter(Context context, Cursor cursor) {
        mContext = context;
        mCursor = cursor;
    }
    public static class ViewHolder extends RecyclerView.ViewHolder {
        EmployerListItemBinding itemBinding;
        public ViewHolder(View itemView) {
            super(itemView);
            itemBinding = DataBindingUtil.bind(itemView);
        }
        public void bindCursor(Cursor cursor) {
            itemBinding.nameLabel.setText(cursor.getString(
                    cursor.getColumnIndexOrThrow(SampleDBContract.Employer.COLUMN_NAME)
            ));
            itemBinding.descLabel.setText(cursor.getString(
                    cursor.getColumnIndexOrThrow(SampleDBContract.Employer.COLUMN_DESCRIPTION)
            ));
            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(cursor.getLong(
                    cursor.getColumnIndexOrThrow(SampleDBContract.Employer.COLUMN_FOUNDED_DATE)));
            itemBinding.foundedLabel.setText(new SimpleDateFormat("dd/MM/yyyy").format(calendar.getTime()));
        }
    }
    @Override
    public int getItemCount() {
        return mCursor.getCount();
    }
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        mCursor.moveToPosition(position);
        holder.bindCursor(myCursor);
    }
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.employer_list_item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }
}
Отображение содержимого объекта Cursor в RecyclerView

Определение внешних ключей

На данный момент в нашем Android SQLite примере мы создали таблицу Employer, которую заполнили строками. Теперь создадим таблицу Employee, которая связана с таблицей Employer через столбец _ID Employer. Мы определяем класс Employee, который расширяет BaseColumns в классе SampleDBContract. Обратите внимание, что при создании таблицы Employee использовали "FOREIGN KEY(employer_id) REFERENCES employer(_id)":

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_DATE_OF_BIRTH = "date_of_birth";
        public static final String COLUMN_EMPLOYER_ID = "employer_id";
        public static final String COLUMN_JOB_DESCRIPTION = "job_description";
        public static final String COLUMN_EMPLOYED_DATE = "employed_date";
        public static final String CREATE_TABLE = "CREATE TABLE " +
                TABLE_NAME + " (" +
                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_FIRSTNAME + " TEXT, " +
                COLUMN_LASTNAME + " TEXT, " +
                COLUMN_DATE_OF_BIRTH + " INTEGER, " +
                COLUMN_EMPLOYER_ID + " INTEGER, " +
                COLUMN_JOB_DESCRIPTION + " TEXT, " +
                COLUMN_EMPLOYED_DATE + " INTEGER, " +
                "FOREIGN KEY(" + COLUMN_EMPLOYER_ID + ") REFERENCES " +
                Employer.TABLE_NAME + "(" + Employer._ID + ") " + ")";
    }

Обновление SQLiteOpenHelper

На данный момент в Android Studio SQLite у вас должна быть создана таблица Employer и в нее добавлены значения. Если вы не изменяете версию базы данных, новая таблица Employee не будет создана. К сожалению, если вы измените версию через повторный вызов метода onUpgrade(), то таблица Employer будет сброшена. Чтобы предотвратить это, можно закомментировать или удалить оператор drop в методе onUpgrade() и добавить оператор execSQL() для создания таблицы Employee. Поскольку таблица Employee ссылается на таблицу Employer, сначала необходимо создать таблицу Employer:

public class SampleDBSQLiteHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 2;
    public static final String DATABASE_NAME = "sample_database";
    public SampleDBSQLiteHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(SampleDBContract.Employer.CREATE_TABLE);
        sqLiteDatabase.execSQL(SampleDBContract.Employee.CREATE_TABLE);
    }
	// Мы не хотим удалять данные пользователей.
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
		if(oldVersion == 0 && newVersion == 2) {
			sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + SampleDBContract.Employee.TABLE_NAME);
		}
		else {
			sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + SampleDBContract.Employer.TABLE_NAME);
			sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + SampleDBContract.Employee.TABLE_NAME);
		}
        onCreate(sqLiteDatabase);
    }
}

Отображение данных из запроса SQLite в Spinner

Чтобы создать работника в таблице Employee, пользователю необходимо выбрать соответствующего работодателя в таблице Employer. Для этого можно предоставить пользователю Spinner. Отобразить содержимое Cursor в Spinner довольно просто.

Сначала мы выполняем Android SQLite query, как было описано выше, выбираем только name из Employer и id (queryCols). Затем создаем экземпляр SimpleCursorAdapter, передавая ему Cursor, массив столбцов для отображения (adapterCols) и массив представлений, с помощью которых должны отображаться столбцы (adapterRowViews). Затем устанавливаем Spinner Adapter для SimpleCursorAdapter:

String[] queryCols = new String[]{"_id", SampleDBContract.Employer.COLUMN_NAME};
        SQLiteDatabase database = new SampleDBSQLiteHelper(this).getReadableDatabase();
        Cursor cursor = database.query(
                SampleDBContract.Employer.TABLE_NAME,     // Запрашиваемая таблица
                queryCols,                                // Возвращаемый столбец
                null,                                     // Столбец для условия WHERE
                null,                                     // Значение для условия WHERE
                null,                                     // не группировать строки
                null,                                     // не фильтровать по группам строк
                null                                      // не сортировать
        );
        String[] adapterCols = new String[]{SampleDBContract.Employer.COLUMN_NAME};
        int[] adapterRowViews = new int[]{android.R.id.text1};
        SimpleCursorAdapter cursorAdapter = new SimpleCursorAdapter(
                this, android.R.layout.simple_spinner_item, cursor, adapterCols, adapterRowViews, 0);
        cursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        binding.employerSpinner.setAdapter(cursorAdapter);
Отображение данных из запроса SQLite в Spinner

Вставка внешнего ключа в базу данных

Вставка строки, содержащей внешний ключ, полностью идентична вставке строк в таблицу без ограничений по внешнему ключу. Разница заключается в том, что в Android SQLite примере мы получаем ссылку на выбранный объект Cursor из Spinner, а затем - значение столбца _ID Employer:

values.put(SampleDBContract.Employee.COLUMN_EMPLOYER_ID, ((Cursor)binding.employerSpinner.getSelectedItem()).getInt(0));
Вставка внешнего ключа в базу данных

Выборка данных из базы SQLite с помощью JOIN

Нельзя использовать метод SQLiteDatabase query() для выполнения запроса к нескольким таблицам. Для этого нужно составить собственный SQL-запрос. В приведенном ниже примере запрос определяется в классе SampleDBContract:

public static final String SELECT_EMPLOYEE_WITH_EMPLOYER = "SELECT * " +
            "FROM " + Employee.TABLE_NAME + " ee INNER JOIN " + Employer.TABLE_NAME + " er " +
            "ON ee." + Employee.COLUMN_EMPLOYER_ID + " = er." + Employer._ID + " WHERE " +
            "ee." + Employee.COLUMN_FIRSTNAME + " like ? AND ee." + Employee.COLUMN_LASTNAME + " like ?";

Обратите внимание, что в условии WHERE мы используем символ «?». Чтобы не нарушить синтаксис SQL нужно определить selectArgs String [] со значениями, которые будут заменять в предоставленном SQL-запросе символ «?»:

private void readFromDB() {
        String firstname = binding.firstnameEditText.getText().toString();
        String lastname = binding.lastnameEditText.getText().toString();
        SQLiteDatabase database = new SampleDBSQLiteHelper(this).getReadableDatabase();
        String[] selectionArgs = {"%" + firstname + "%", "%" + lastname + "%"};
        Log.d(TAG, SampleDBContract.Employee.QUERY_WITH_EMPLOYER);
        Cursor cursor = database.rawQuery(SampleDBContract.Employee.QUERY_WITH_EMPLOYER, selectionArgs);
        Log.d(TAG, "The total cursor count is " + cursor.getCount());
        binding.recycleView.setAdapter(new SampleJoinRecyclerViewCursorAdapter(this, cursor));
    }
Выборка данных из базы SQLite с помощью JOIN

В заключении

Полная версия исходного кода доступна на github для использования и изменения. Базы данных Android SQLite - это мощное средство, доступное для всех мобильных приложений.

Вадим Дворниковавтор-переводчик статьи «Using a simple SQLite database in your Android app»