Вы когда-нибудь работали с миграциями Rails ? Они делают изменения базы данных легким делом , не так ли? В то время как каждый выпуск программного обеспечения не обязательно включает в себя миграцию, когда кто-то использует его, я всегда рад тому, как легко все получается. Будь то добавление новых данных или изменение существующих структур данных, миграция Rails делает создание хранилища данных (будь то RDMBS или NoSQL, например MongoDB) безболезненным.
Когда я недавно обнаружил, что изменяю структуру данных базы данных SQLite для одного из моих приложений для Android , я захотел, чтобы существовал такой же механизм миграции для Android, как и в Rails. Увы, я не смог ничего сделать, поэтому я сделал то, что сделал бы любой другой разработчик: я написал один .
Droid Migrate — это простая среда командной строки, которая генерирует и запускает миграции баз данных для ваших приложений Android, использующих SQLite . Миграция инкапсулируется классом DBVersion
который содержит метод up
и down
. Метод up
вызывается для обновления, а down
для отката. Что эти методы делают, полностью зависит от вас.
Кроме того, Droid Migrate генерирует класс DatabaseHelper
помощью которого вы получаете базовые соединения с экземпляром SQLite — это в любом случае канонический способ взаимодействия с SQLite в приложении Android , но с Droid Migrate вы получаете специально улучшенный компонент DatabaseHelper
который определяет, какая версия целевого экземпляра базы данных является самым последним и выполняет соответствующие миграции для приведения базы данных к этой версии.
Таким образом, с помощью только что созданного вами класса DatabaseHelper
вы все равно можете взаимодействовать с базой данных вашего приложения, как обычно, однако, используя этот класс, все миграции выполняются за вас. Позвольте мне продемонстрировать.
Я создал простое приложение, которое на данный момент не взаимодействует с какой-либо базой данных — оно просто создает ListView
который предназначен для хранения списка записей для просмотра. Вы можете найти это приложение на Github, если хотите подписаться. Тем не менее, основная активность приложения показана ниже.
Простое приложение для Android без какой-либо логики SQLite
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package com.b50.db.ex; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = (TextView) findViewById(R.id.textView1); textView.setText( "This would be a list from a DB if there was a DB" ); } } |
Что я хотел бы сделать, это добавить возможность взаимодействия с базой данных SQLite; Кроме того, я хотел бы иметь возможность развивать модель данных в последующих выпусках. Именно здесь сияет Droid Migrate.
После того, как я установил Droid Migrate (просто клонируйте или загрузите код, соберите его и поместите в свой PATH
и создайте новую переменную среды, названную DROID_MIGRATE_HOME
), я могу инициализировать свое приложение для использования Droid Migrate, открыв терминал в корневом DROID_MIGRATE_HOME
моего приложения и набрав:
Инициализация Droid Migrate
1
|
$> droid-migrate init -d a_catalog |
Флаг -d
указывает имя моей желаемой базы данных. При желании я могу указать имя пакета через флаг -p
если я хочу, чтобы мои новые сгенерированные классы были в отдельном пакете от моего основного приложения.
Если вы посмотрите на код вашего приложения, вы должны заметить ряд новых вещей. Сначала вы увидите два новых класса и новый файл jar. Классы — вышеупомянутый DatabaseHelper
и класс, названный DBVersion1
. Недавно добавленный jar-файл в папке libs
вашего приложения содержит несколько классов, которые соответствуют зависимостям времени выполнения Droid Migrate — этот jar-файл чрезвычайно компактен и занимает 4 КБ.
Класс DatabaseHelper
очень прост:
DatabaseHelper не может быть проще
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package com.b50.db.ex; import com.b50.db.ex.R; import android.content.Context; import com.b50.migrations.MigrationsDatabaseHelper; public class DatabaseHelper extends MigrationsDatabaseHelper { public DatabaseHelper(Context context) { super (context, context.getString(R.string.database_name), null , context.getResources().getInteger(R.integer.database_version), context.getString(R.string.package_name)); } } |
Этот класс расширяет MigrationsDatabaseHelper
Droid Migrate, который в конечном итоге расширяет SQLiteOpenHelper
для Android, так что, как я упоминал ранее, у вас есть все, что вам нужно для взаимодействия с SQLite, у вас под рукой через DatabaseHelper
. Если вы посмотрите внимательно, то увидите, что этот класс использует специализированный XML-файл (который в конечном итоге генерируется в вашем классе R
).
Загляните в папку res/values
и откройте только что созданный файл migrations.xml
. Это должно выглядеть примерно так:
migrations.xml содержит версию базы данных, имя пакета и имя базы данных
1
2
3
4
5
6
|
<?xml version= "1.0" encoding= "utf-8" ?> <resources> <integer name= "database_version" > 1 </integer> <string name= "database_name" >a_catalog</string> <string name= "package_name" >com.b50.db.ex</string> </resources> |
Обратите внимание на значение database_version
— это 1. Это соответствует DBVersion1
классу DBVersion1
. Взгляните на этот класс:
DBVersion1 — ваш начальный класс миграции
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package com.b50.db.ex; import com.b50.migrations.AbstractMigration; public class DBVersion1 extends AbstractMigration { public void up() { //execSQL("some sql create stmts"); } public void down() { //execSQL("some delete sql stmts"); } } |
В этом классе вы реализуете свою первоначальную миграцию, которая будет создавать различные таблицы и заполнять их. Используйте метод execSQL
для передачи допустимой String
SQL. Например, я создам начальную миграцию так:
DBVersion1 теперь реализован
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
package com.b50.db.ex; import com.b50.migrations.AbstractMigration; public class DBVersion1 extends AbstractMigration { public void up() { String create = "CREATE TABLE hops (_id integer PRIMARY KEY AUTOINCREMENT DEFAULT NULL, name TEXT, description TEXT, substitutions TEXT DEFAULT '', alpha_acid TEXT DEFAULT '', beer_styles TEXT DEFAULT '', type TEXT DEFAULT '', user_notes TEXT DEFAULT '');" ; execSQL(create); String oneThing = "INSERT INTO 'hops' VALUES(1,'Amarillo','Spicy hop with mild bitterness and a noble aroma. Good all around hop.','Cascade, Centennial','7 to 10','Ale, IPA','Aroma', '');" ; execSQL(oneThing); } public void down() { execSQL( "DROP TABLE hops;" ); } } |
Как видите, мой метод up
создает таблицу и вставляет одну запись. Мой метод down
откатывает вещи назад, что в данном случае означает удаление созданной таблицы.
Теперь все, что мне нужно сделать, это использовать экземпляр DatabaseHelper
моего приложения, и Droid Migrate обеспечит правильную инициализацию. Поэтому я обновлю исходное действие, чтобы отобразить список того, что находится в базе данных, и я знаю, что это только одна запись, основанная на моей первоначальной миграции.
Обновлена активность для взаимодействия с SQLite
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
|
package com.b50.db.ex; import android.app.Activity; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SimpleCursorAdapter; public class MainActivity extends Activity { protected SQLiteDatabase db; protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); this .db = ( new DatabaseHelper( this )).getWritableDatabase(); ListView list = (ListView) findViewById(R.id.list); ListAdapter adapter = getAdaptorForQuery( "SELECT _id, name, description FROM hops ORDER BY name ASC" , null ); list.setAdapter(adapter); } private ListAdapter getAdaptorForQuery(String queryString, String[] parameters) { Cursor cursor = this .db.rawQuery(queryString, parameters); return new SimpleCursorAdapter( this , R.layout.list_item, cursor, new String[] { "name" , "description" }, new int [] { R.id.hopName, R.id.description }, 0 ); } } |
Как видно из приведенного выше кода, приложение теперь выполняет запрос к базовому экземпляру SQLite и создает ListView
из набора результатов запроса.
Ключевая строка — это способ SQLiteDatabase
экземпляра SQLiteDatabase
: this.db = (new DatabaseHelper(this)).getWritableDatabase();
— вот где происходит вся магия. Droid Migrate передает номер версии на платформу Android, и в случае изменения платформа Android вызывает серию методов жизненного цикла, которые Droid Migrate связывает с вашими миграциями.
Например, давайте представим, что следующий выпуск этого приложения добавляет больше данных в таблицу hops
. Поэтому я сгенерирую новую миграцию. Это можно сделать, введя следующую команду в корневой каталог вашего проекта следующим образом:
Генерация новой миграции
1
|
$> droid-migrate generate up |
Флаг « up
означает увеличение версии базы данных (например, версия ++), а « down
— откат (например, версия–). Если вы посмотрите на код вашего приложения, вы заметите новый класс: DBVersion2
и ваш файл migrations.xml
был обновлен: значение database_version
теперь равно 2.
Я реализую свой класс DBVersion2
следующим образом:
Реализация DBVersion2 для добавления еще одной строки данных
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package com.b50.db.ex; import com.b50.migrations.AbstractMigration; public class DBVersion2 extends AbstractMigration { public void up() { execSQL( "INSERT INTO 'hops' VALUES(100,'Zythos','New IPA style hop blend created to optimize and exceed the aroma characteristics of the traditional, and sometimes hard to get, IPA hops.','Amarillo, Columbus, Cascade','9.5 to 12','IPAs','Bittering and Aroma', '');" ); } public void down() { execSQL( "DELETE from 'hops' where _id = 100" ); } } |
Теперь, если я запусту свое приложение, в ListView
будет 2 элемента!
Что делать, если вам нужно откат? Это так же просто. Представьте, что добавление этой второй строки данных было гигантской ошибкой, и вместо этого я действительно хочу только одну строку (т.е. я хочу только данные, изначально созданные в DBVersion1
). Все, что мне нужно сделать, это ввести в корне моего проекта:
Откат в Droid Migrate так же прост
1
|
$> droid-migrate generate down |
После ввода вышеуказанной команды вы должны увидеть следующий вывод:
Откат к версии 1!
1
2
3
|
Generating a rollback migration... Rolling back your migrations.xml file to indicate database version 1 Done! |
Единственное, что изменится в вашем проекте, это файл migrations.xml
— значение database_version
будет возвращено к 1 (или тому, что когда-либо будет 1 минус текущая версия).
DBVersion2
приложение снова, и вот: одно значение отображается, потому что был выполнен метод down
DBVersion2
!
Droid Migrate делает обновления и откаты к вашей базовой базе данных SQLite на одном дыхании; Более того, он может обрабатывать обновления или откаты за пределами одной версии. То есть, если экземпляр приложения обновляется с версии 2 до версии 6, каждая миграция будет выполняться по порядку (3, 4, 5 и 6). То же самое относится и к откату.
Если вы работаете с SQLite в приложении для Android, я настоятельно рекомендую вам взглянуть на Droid Migrate !