Статьи

Сохранение в базе данных SQLite в вашем приложении Android

Это четвертый пост в моей серии о сохранении данных в приложениях Android. Вот другие посты:

Введение: Как сохранить данные в вашем Android-приложении
Сохранение данных в файл в вашем приложении Android
Сохранение настроек в вашем приложении Android

В предыдущих статьях описывалось, как сохранять файлы в файловой системе и в файлах настроек. Этого может быть достаточно, если речь идет о простом приложении, но если ваши данные имеют сложную структуру или у вас много данных для сохранения, лучше использовать базу данных. Управление базой данных требует больше знаний и настройки, но оно поставляется с множеством проверок и оптимизацией производительности. Android SDK включает в себя движок базы данных SQLite с открытым исходным кодом и классы, необходимые для доступа к нему.

SQLite — это автономная реляционная база данных, для работы которой не требуется сервер. Сама база данных сохраняется в файл во внутреннем хранилище вашего приложения, поэтому каждое приложение имеет свою собственную частную базу данных, которая недоступна для других приложений. Вы можете узнать больше о самом проекте SQLite и его реализации языка запросов SQL на http://www.sqlite.org .

Новое в базах данных? Реляционная база данных сохраняет данные в таблицы. Каждая таблица состоит из столбцов, и для каждого столбца вы должны выбрать имя и тип данных, которые могут быть сохранены в нем. В каждой таблице также должен быть столбец или множество столбцов, которые задаются в качестве ключа таблицы, чтобы каждая строка данных могла быть однозначно идентифицирована. Отношения также могут быть определены между таблицами.

Основы баз данных и язык запросов SQL, используемые большинством баз данных, могут быть использованы для объяснения. Если вы не знаете, как использовать базу данных, об этом стоит узнать больше, поскольку базы данных используются практически во всех приложениях для хранения данных.

Чтобы продемонстрировать, как создать базу данных и взаимодействовать с ней, я создал небольшой пример приложения, которое доступно по адресу http://github.com/CindyPotvin/RowCounter . Приложение представляет собой счетчик строк для проектов вязания: пользователь может создать проект вязания, содержащий один или несколько счетчиков, используемых для отслеживания текущего числа выполненных рядов и отображения общего количества строк, которые должны быть достигнуты. Структура базы данных следующая: таблица проекта связана с таблицей row_counter :

rowcounter-1024x456
Во-первых, чтобы иметь возможность создавать базу данных, нам нужен класс контракта для каждой таблицы, который описывает имя элементов таблицы. Этот класс следует использовать каждый раз, когда требуется имя элементов в базе данных. Чтобы описать имя каждого столбца, класс контракта также содержит подкласс с реализацией android.provider.BaseColumn, который автоматически добавляет имя столбца _ID и _COUNT . Мне также нравится помещать SQL-запрос CREATE TABLE в класс контракта, чтобы все строки, используемые в SQL-запросах, были в одном месте. Вот класс контракта для таблицы row_counter в примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* This class represents a contract for a row_counter table containing row
* counters for projects. The project must exist before creating row counters
* since the counter have a foreign key to the project.
*/
public final class RowCounterContract {
 
/**
* Contains the name of the table to create that contains the row counters.
*/
public static final String TABLE_NAME = "row_counter";
 
/**
* Contains the SQL query to use to create the table containing the row counters.
*/
public static final String SQL_CREATE_TABLE = "CREATE TABLE "
+ RowCounterContract.TABLE_NAME + " ("
+ RowCounterContract.RowCounterEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_PROJECT_ID + " INTEGER,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_CURRENT_AMOUNT + " INTEGER DEFAULT 0,"
+ RowCounterContract.RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT + " INTEGER,"
+ "FOREIGN KEY (" + RowCounterContract.RowCounterEntry.COLUMN_NAME_PROJECT_ID + ") "
+ "REFERENCES projects(" + ProjectContract.ProjectEntry._ID + "));";
 
/**
* This class represents the rows for an entry in the row_counter table. The
* primary key is the _id column from the BaseColumn class.
*/
public static abstract class RowCounterEntry implements BaseColumns {
 
   // Identifier of the project to which the row counter belongs
   public static final String COLUMN_NAME_PROJECT_ID = "project_id";
 
   // Final amount of rows to reach
  public static final String COLUMN_NAME_FINAL_AMOUNT = "final_amount";
 
   // Current amount of rows done
   public static final String COLUMN_NAME_CURRENT_AMOUNT = "current_amount";
   }
}

Чтобы создать таблицы, в которых хранятся данные, описанные в контрактах, вы должны реализовать класс android.database.sqllite.SQLLiteOpenHelper, который управляет доступом к базе данных. Следующие методы должны быть реализованы по мере необходимости:

  • onCreate: этот метод вызывается при первом открытии базы данных вашим приложением. Вы должны настроить базу данных для использования в этом методе, создав таблицы и инициализировав все необходимые данные.
  • onUpdate: этот метод вызывается при обновлении приложения и изменении номера версии. Вам не нужно ничего делать для вашей первой версии, но в следующих версиях вы должны предоставить запросы для изменения базы данных со старой версии на новую структуру по мере необходимости, чтобы ваш пользователь не потерял свои данные во время обновления.
  • onDowngrade (необязательно): вы можете реализовать этот метод, если хотите обработать случай, когда ваше приложение понижено до версии, требующей более старой версии. Реализация по умолчанию создаст исключение SQLiteException и не изменит базу данных.
  • onOpen (необязательно): этот метод вызывается после того, как база данных была создана, обновлена ​​до более новой версии или понижена до более старой версии.

Вот базовая реализация android.database.sqllite.SQLLiteOpenHelper для примера, который выполняет запрос SQL CREATE TABLE для каждой таблицы базы данных в методе onCreate . В классе android.database.sqlite.SQLiteDatabase нет метода для создания таблицы, поэтому для выполнения запроса необходимо использовать метод execSQL .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* This class helps open, create, and upgrade the database file containing the
* projects and their row counters.
*/
public class ProjectsDatabaseHelper extends SQLiteOpenHelper {
   // If you change the database schema, you must increment the database version.
   public static final int DATABASE_VERSION = 1;
   // The name of the database file on the file system
   public static final String DATABASE_NAME = "Projects.db";
 
   public ProjectsDatabaseHelper(Context context) {
      super(context, DATABASE_NAME, null, DATABASE_VERSION);
      }
 
   /**
    * Creates the underlying database with the SQL_CREATE_TABLE queries from
    * the contract classes to create the tables and initialize the data.
    * The onCreate is triggered the first time someone tries to access
    * the database with the getReadableDatabase or
    * getWritableDatabase methods.
    *
    * @param db the database being accessed and that should be created.
    */
    @Override
   public void onCreate(SQLiteDatabase db) {
      // Create the database to contain the data for the projects
      db.execSQL(ProjectContract.SQL_CREATE_TABLE);
      db.execSQL(RowCounterContract.SQL_CREATE_TABLE);
      initializeExampleData(db);
      }
 
   /**
    * This method must be implemented if your application is upgraded and must
    * include the SQL query to upgrade the database from your old to your new
    * schema.
    *
    * @param db the database being upgraded.
    * @param oldVersion the current version of the database before the upgrade.
    * @param newVersion the version of the database after the upgrade.
    */
   @Override
   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
      // Logs that the database is being upgraded
      Log.i(ProjectsDatabaseHelper.class.getSimpleName(),
            "Upgrading database from version " + oldVersion + " to " + newVersion);
      }
   }

После реализации android.database.sqllite.SQLLiteOpenHelper вы можете получить экземпляр объекта базы данных android.database.sqlite.SQLiteDatabas e, используя метод getReadableDatabase помощника, если вам нужно только прочитать данные или метод getWritableDatabase, если вам нужно читать и записывать данные. Существует четыре вида основных операций, которые можно выполнять с данными, и модификации не могут быть отменены, как во всех базах данных.

  • Вставка новой строки: метод вставки объекта android.database.sqlite.SQLiteDatabase вставляет новую строку данных в таблицу. Данные могут быть вставлены с помощью запроса SQL INSERT с использованием метода execSQL , но рекомендуется использовать вставку, чтобы избежать внедрения SQL: методом вставки может быть создана только одна строка базы данных и ничего больше, независимо от входных данных. В следующем примере несколько тестовых проектов инициализируются в базе данных приложения методом onCreate помощника базы данных после создания таблицы:
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    /**
     * Initialize example data to show when the application is first installed.
     *
     * @param db the database being initialized.
     */
    private void initializeExampleData(SQLiteDatabase db) {
       // A lot of code is repeated here that could be factorized in methods,
       // but this is clearer for the example
             
       // Insert the database row for an example project in the project table in the
       // database
       long projectId;
       ContentValues firstProjectValues = new ContentValues();
       firstProjectValues.put(ProjectContract.ProjectEntry.COLUMN_NAME_TITLE,
                              "Flashy Scarf");
       projectId = db.insert(ProjectContract.TABLE_NAME, null, firstProjectValues);
       // Insert the database rows for a row counter linked to the project row
       // just created in the database (the insert method returns the
       // identifier of the row)
       ContentValues firstProjectCounterValues = new ContentValues();
       firstProjectCounterValues.put(RowCounterContract
                                       .RowCounterEntry.COLUMN_NAME_PROJECT_ID, projectId);
       firstProjectCounterValues.put(RowCounterContract
                                       .RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT, 120);
       db.insert(RowCounterContract.TABLE_NAME, null, firstProjectCounterValues);
             
       // Insert the database row for a second example project in the project
       // table in the database.
       ContentValues secondProjectValues = new ContentValues();
       secondProjectValues.put(ProjectContract.ProjectEntry.COLUMN_NAME_TITLE,
                               "Simple Socks");
       projectId = db.insert(ProjectContract.TABLE_NAME, null, secondProjectValues);
       // Insert the database rows for two identical row counters for the
       // project in the database
       ContentValues secondProjectCounterValues = new ContentValues();
       secondProjectCounterValues.put(RowCounterContract
                                        .RowCounterEntry.COLUMN_NAME_PROJECT_ID, projectId);
       secondProjectCounterValues.put(RowCounterContract
                                        .RowCounterEntry.COLUMN_NAME_FINAL_AMOUNT, 80);
       db.insert(RowCounterContract.TABLE_NAME, null, secondProjectCounterValues);
       db.insert(RowCounterContract.TABLE_NAME, null, secondProjectCounterValues); 
       }
  • Чтение существующих строк: метод запроса из класса android.database.sqlite.SQLiteDatabase извлекает данные, которые ранее были вставлены в базу данных. Этот метод возвращает курсор, который указывает на коллекцию строк, возвращаемых вашим запросом, если таковые имеются. Затем вы можете преобразовать данные, извлеченные из таблицы базы данных, в объект, который можно использовать в вашем приложении: в этом примере строки таблицы проекта преобразуются в объекты проекта .
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /**
     
    * Gets the list of projects from the database.
    *
    * @return the current projects from the database.
    */
    public ArrayList getProjects() {
       ArrayList projects = new ArrayList();
       // Gets the database in the current database helper in read-only mode
       SQLiteDatabase db = getReadableDatabase();
     
       // After the query, the cursor points to the first database row
       // returned by the request.
       Cursor projCursor = db.query(ProjectContract.TABLE_NAME, null, null,
                                    null, null, null, null);
       while (projCursor.moveToNext()) {
          // Get the value for each column for the database row pointed by
          // the cursor using the getColumnIndex method of the cursor and
          // use it to initialize a Project object by database row
          Project project = new Project();
           
          int idColIndex = projCursor.getColumnIndex(ProjectContract.ProjectEntry._ID);
          long projectId = projCursor.getLong(idColIndex);
          project.setId(projCursor.getLong(projectId);
     
          int nameColIndex = projCursor.getColumnIndex(ProjectContract
                                                        .ProjectEntry.COLUMN_NAME_TITLE);
          project.setName(projCursor.getString(nameColIndex));
          // Get all the row counters for the current project from the
          // database and add them all to the Project object
         project.setRowCounters(getRowCounters(projectId));
     
         projects.add(project);
         }
       return (projects);
       }
  • Обновление существующих строк: метод update экземпляра класса android.database.sqlite.SQLiteDatabase обновляет данные в строке или в нескольких строках таблицы базы данных. Как и в случае метода вставки , вы можете использовать запрос execSQL для выполнения запроса SQL UPDATE, но использовать метод обновления безопаснее. В следующем примере текущее значение счетчика строк для счетчика строк в таблице row_counter обновляется новым значением. В соответствии с указанным условием обновляется только счетчик строк с идентификатором, переданным в качестве параметра, но с другим условием вы можете обновить много строк, поэтому вы всегда должны убедиться, что условие выбирает только те строки, которые вам нужны.
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
     * Updates the current amount of the row counter in the database to the value
     * in the object passed as a parameter.
     *
     * @param rowCounter the object containing the current amount to set.
     */
    public void updateRowCounterCurrentAmount(RowCounter rowCounter) {
       SQLiteDatabase db = getWritableDatabase();
             
       ContentValues currentAmountValue = new ContentValues();
       currentAmountValue.put(RowCounterContract.RowCounterEntry.COLUMN_NAME_CURRENT_AMOUNT,
                              rowCounter.getCurrentAmount());
             
       db.update(RowCounterContract.TABLE_NAME,
             currentAmountValue,
             RowCounterContract.RowCounterEntry._ID +"=?",
             new String[] { String.valueOf(rowCounter.getId()) });
       }
  • Удаление существующих строк: метод delete для экземпляра класса android.database.sqlite.SQLiteDatabase удаляет строку или несколько строк таблицы базы данных. Как и в случае метода вставки , вы можете использовать запрос execSQL для выполнения запроса SQL UPDATE, но использование метода удаления безопаснее. В следующем примере счетчик строк в таблице row_counter удаляется. В соответствии с указанным условием удаляется только счетчик строк с идентификатором, переданным в качестве параметра, но с другим условием вы можете удалить много строк, поэтому всегда следите за тем, чтобы условие выбирало только те строки, которые вам нужны, чтобы не удалять их много данных.
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    /**
     * Deletes the specified row counter from the database.
     *
     * @param rowCounter the row counter to remove.
     */
    public void deleteRowCounter(RowCounter rowCounter) {
       SQLiteDatabase db = getWritableDatabase();
             
       db.delete(RowCounterContract.TABLE_NAME,              
                 RowCounterContract.RowCounterEntry._ID +"=?",
                 new String[] { String.valueOf(rowCounter.getId()) });
       }

Наконец, если вы хотите инкапсулировать доступ к данным в вашей базе данных, чтобы избежать вызова помощника базы данных непосредственно в вашей деятельности, вы также можете реализовать класс android.content.ContentProvider из Android SDK. Это требуется только в том случае, если ваше приложение должно делиться данными с другими приложениями: вам не нужно их начинать, но вы должны рассмотреть возможность их использования, поскольку ваши данные становятся более сложными.