В прошлой статье мы разобрались, для чего нам нужна кодогенерация, но пока несколько смутно представляем, что именно мы собираемся кодогенерировать. Исправим это.
Кодогенерация - какая она?
Не знаю как у Вас, но у меня вновь созданный, абсолютно пустой проект вызывает чувство необузданного первобытного ужаса :( Чтобы его пересилить и начать писать хоть что-то, проще всего начинать с самого простого и понятного. Например, мне абсолютно понятно, что наш проект реализует Content Provider и никак не сможет обойтись без трех служебных таблиц, описанных в последней диаграмме прошлой статьи. Опишем эти таблицы в Java-коде:
package com.WhiteRabbit.Codegen;
...
public final class Db {
private final static String SCHEME = "content://";
public final static String SITE = "com.WhiteRabbit.";
public final static String APP = "Codegen";
public final static String AUTHORITY = SITE + "provider." + APP;
public final static String DIR_TYPE = "vnd.android.cursor.dir/vnd." + AUTHORITY + ".";
public final static String ITEM_TYPE = "vnd.android.cursor.item/vnd." + AUTHORITY + ".";
public final static String DATA_PATH = "data";
public final static Uri DATA_CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + "/" + DATA_PATH);
public final static String DATA_ITEM_TYPE = ITEM_TYPE + DATA_PATH;
public final static String ID_COLUMN_NAME = "_id";
public final static String VERSION_COLUMN_NAME = "version";
private Db() {}
public final static class DataType {
public final static int INTEGER = 1;
public final static int TEXT = 2;
}
public final static class Table implements BaseColumns {
public final static String PATH = "Table";
public final static Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
private Table() {}
public final static String TABLE_NAME = "_table";
public final static String NAME_COLUMN = "table_name";
public final static String ALIAS_COLUMN = "alias_name";
}
public final static class Version implements BaseColumns {
public final static String PATH = "Version";
public final static Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
private Version() {}
public final static String TABLE_NAME = "_version";
public final static String VER_DATE_COLUMN = "version_date";
}
public final static class Column implements BaseColumns {
public final static String PATH = "Column";
public final static Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + "/" + PATH);
public final static String CONTENT_DIR_TYPE = DIR_TYPE + PATH;
public final static String CONTENT_ITEM_TYPE = ITEM_TYPE + PATH;
private Column() {}
public final static String TABLE_NAME = "_column";
public final static String TABLE_ID_COLUMN = "table_id";
public final static String TYPE_ID_COLUMN = "data_type";
public final static String IS_NOT_NULL = "is_not_null";
public final static String NAME_COLUMN = "column_name";
public final static String ALIAS_COLUMN = "alias_name";
}
}
Далее тривиально создаем эти таблицы в ProviderMetadata:
package com.WhiteRabbit.Codegen;
...
public abstract class ProviderMetadata extends ContentProvider {
protected DatabaseHelper dbHelper;
static abstract class DatabaseHelper extends SQLiteOpenHelper {
private int version;
DatabaseHelper(Context context, String name, int version) {
super(context, name, null, version);
this.version = version;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + Db.Version.TABLE_NAME + " (" +
Db.Version._ID + " INTEGER PRIMARY KEY," +
Db.Version.VER_DATE_COLUMN + " TEXT);");
db.execSQL("CREATE TABLE " + Db.Table.TABLE_NAME + " (" +
Db.Table._ID + " INTEGER PRIMARY KEY," +
Db.VERSION_COLUMN_NAME + " INTEGER NOT NULL," +
Db.Table.ALIAS_COLUMN + " TEXT NOT NULL," +
Db.Table.NAME_COLUMN + " TEXT NOT NULL);");
db.execSQL("CREATE TABLE " + Db.Column.TABLE_NAME + " (" +
Db.Column._ID + " INTEGER PRIMARY KEY," +
Db.VERSION_COLUMN_NAME + " INTEGER NOT NULL," +
Db.Column.TABLE_ID_COLUMN + " INTEGER NOT NULL," +
Db.Column.TYPE_ID_COLUMN + " INTEGER NOT NULL," +
Db.Column.IS_NOT_NULL + " INTEGER NOT NULL," +
Db.Column.ALIAS_COLUMN + " TEXT NOT NULL," +
Db.Column.NAME_COLUMN + " TEXT NOT NULL);");
onUpgrade(db, 0, version);
}
}
}
Теперь начинается самое интересное. Создаем файл, который впоследствии будем генерировать автоматически (соответственно, он должен быть построен таким образом, чтобы максимально облегчить эту кодогенерацию):
package com.WhiteRabbit.Codegen;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public abstract class ProviderPatches extends ProviderMetadata {
protected static final int DATABASE_VERSION = 1;
static class PatchedDatabaseHelper extends DatabaseHelper {
PatchedDatabaseHelper(Context context, String name, int version) {
super(context, name, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public abstract class ProviderPatches extends ProviderMetadata {
protected static final int DATABASE_VERSION = 1;
static class PatchedDatabaseHelper extends DatabaseHelper {
PatchedDatabaseHelper(Context context, String name, int version) {
super(context, name, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
if (oldVersion < 1) {patch_1(db);}
}
private void patch_1(SQLiteDatabase db) {
long tableId;
addVersion(db, 1);
tableId = addTable(db,
if (oldVersion < 1) {patch_1(db);}
}
private void patch_1(SQLiteDatabase db) {
long tableId;
addVersion(db, 1);
tableId = addTable(db,
Db.TestTable.TABLE_NAME,
"Db.TestTable.TABLE_NAME", 1);
addColumnNotNull(db, tableId,
addColumnNotNull(db, tableId,
Db.DataType.TEXT, Db.TestTable.FIELD_COLUMN,
"Db.TestTable.FIELD_COLUMN", 1);
createTable(db, tableId);
ContentValues values;
values = new ContentValues();
values.put(Db.TestTable.FIELD_COLUMN, "test");
addData(db, Db.TestTable.TABLE_NAME, values, 1);
}
}
}
createTable(db, tableId);
ContentValues values;
values = new ContentValues();
values.put(Db.TestTable.FIELD_COLUMN, "test");
addData(db, Db.TestTable.TABLE_NAME, values, 1);
}
}
}
Класс этот абстрактный (от него будет наследоваться класс, реализующий функционал нашего Content Provider-а). Также можно заметить, что он определяет номер версии БД DATABASE_VERSION, но имя БД мы до сих пор пока нигде не указывали (поскольку весь ранее описанный код достаточно универсален и может использоваться в различных проектах).
Для создания таблиц и заполнения их данными мы не используем SQL-запросы непосредственно, а используя некое API, создаем описание таблиц или данных, после чего генерируем их в соответствии с этим описанием (такой подход существенно обегчит нам задачу кодогенерации). Определим функции, испольованные нами для генерации данных в ранее созданном классе ProviderMetadata:
package com.WhiteRabbit.Codegen;
...
public abstract class ProviderMetadata extends ContentProvider {
...
static abstract class DatabaseHelper extends SQLiteOpenHelper {
protected void addVersion(SQLiteDatabase db, long id) {
Date now = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
db.execSQL("insert into " + Db.Version.TABLE_NAME + "(" +
Db.Version._ID + "," +
Db.Version.VER_DATE_COLUMN + ") values " +
"(" + Long.toString(id) + ", '" +
...
public abstract class ProviderMetadata extends ContentProvider {
...
static abstract class DatabaseHelper extends SQLiteOpenHelper {
protected void addVersion(SQLiteDatabase db, long id) {
Date now = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
db.execSQL("insert into " + Db.Version.TABLE_NAME + "(" +
Db.Version._ID + "," +
Db.Version.VER_DATE_COLUMN + ") values " +
"(" + Long.toString(id) + ", '" +
formatter.format(now) + "')");
}
protected long getVersion(SQLiteDatabase db) {
Cursor q = db.rawQuery("select " + Db.Version._ID + " " +
"from " + Db.Version.TABLE_NAME + " " +
"where " + Db.Version.VER_DATE_COLUMN + " is null",
null);
if (q.moveToFirst()) {
int idColumn = q.getColumnIndex(Db.Version._ID);
return q.getInt(idColumn);
}
ContentValues values = new ContentValues();
return db.insert(Db.Version.TABLE_NAME, Db.Version._ID, values);
}
public int closeVersions(SQLiteDatabase db) {
Date now = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
ContentValues values = new ContentValues();
values.put(Db.Version.VER_DATE_COLUMN, formatter.format(now));
return db.update(Db.Version.TABLE_NAME, values,
Db.Version.VER_DATE_COLUMN + " is null", null);
}
protected long addTable(SQLiteDatabase db, String name,
}
protected long getVersion(SQLiteDatabase db) {
Cursor q = db.rawQuery("select " + Db.Version._ID + " " +
"from " + Db.Version.TABLE_NAME + " " +
"where " + Db.Version.VER_DATE_COLUMN + " is null",
null);
if (q.moveToFirst()) {
int idColumn = q.getColumnIndex(Db.Version._ID);
return q.getInt(idColumn);
}
ContentValues values = new ContentValues();
return db.insert(Db.Version.TABLE_NAME, Db.Version._ID, values);
}
public int closeVersions(SQLiteDatabase db) {
Date now = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
ContentValues values = new ContentValues();
values.put(Db.Version.VER_DATE_COLUMN, formatter.format(now));
return db.update(Db.Version.TABLE_NAME, values,
Db.Version.VER_DATE_COLUMN + " is null", null);
}
protected long addTable(SQLiteDatabase db, String name,
String alias, int ver) {
ContentValues values = new ContentValues();
values.put(Db.Table.NAME_COLUMN, name);
values.put(Db.Table.ALIAS_COLUMN, alias);
values.put(Db.VERSION_COLUMN_NAME, ver);
return db.insert(Db.Table.TABLE_NAME, Db.Table._ID, values);
}
protected void addColumn(SQLiteDatabase db, long tableId, int typeId,
ContentValues values = new ContentValues();
values.put(Db.Table.NAME_COLUMN, name);
values.put(Db.Table.ALIAS_COLUMN, alias);
values.put(Db.VERSION_COLUMN_NAME, ver);
return db.insert(Db.Table.TABLE_NAME, Db.Table._ID, values);
}
protected void addColumn(SQLiteDatabase db, long tableId, int typeId,
String name, String alias, int ver, boolean isNotNull,
boolean isDefNull) {
ContentValues values = new ContentValues();
values.put(Db.Column.TABLE_ID_COLUMN, tableId);
values.put(Db.Column.TYPE_ID_COLUMN, typeId);
values.put(Db.Column.NAME_COLUMN, name);
values.put(Db.Column.ALIAS_COLUMN, alias);
values.put(Db.Column.IS_NOT_NULL, isNotNull?1:0);
values.put(Db.VERSION_COLUMN_NAME, ver);
db.insert(Db.Column.TABLE_NAME, Db.Column._ID, values);
}
protected void addColumn(SQLiteDatabase db, long tableId,
ContentValues values = new ContentValues();
values.put(Db.Column.TABLE_ID_COLUMN, tableId);
values.put(Db.Column.TYPE_ID_COLUMN, typeId);
values.put(Db.Column.NAME_COLUMN, name);
values.put(Db.Column.ALIAS_COLUMN, alias);
values.put(Db.Column.IS_NOT_NULL, isNotNull?1:0);
values.put(Db.VERSION_COLUMN_NAME, ver);
db.insert(Db.Column.TABLE_NAME, Db.Column._ID, values);
}
protected void addColumn(SQLiteDatabase db, long tableId,
int typeId, String name, String alias, int ver) {
addColumn(db, tableId, typeId, name, alias, ver, false, false);
}
protected void addColumnNotNull(SQLiteDatabase db, long tableId,
addColumn(db, tableId, typeId, name, alias, ver, false, false);
}
protected void addColumnNotNull(SQLiteDatabase db, long tableId,
int typeId, String name, String alias, int ver) {
addColumn(db, tableId, typeId, name, alias, ver, true, false);
}
addColumn(db, tableId, typeId, name, alias, ver, true, false);
}
protected void addData(SQLiteDatabase db, String tableName,
ContentValues values, int ver) {
values.put(Db.VERSION_COLUMN_NAME, ver);
db.insert(tableName, "_id", values);
}
public String getTableName(SQLiteDatabase db, String tableId) {
String r = "";
Cursor q = db.rawQuery("select " + Db.Table.NAME_COLUMN + " " +
"from " + Db.Table.TABLE_NAME + " " +
"where " + Db.Table._ID + " = ?",
new String [] {tableId});
if (q.moveToFirst()) {
int nameColumn = q.getColumnIndex(Db.Table.NAME_COLUMN);
r = q.getString(nameColumn);
}
return r;
}
protected void createTable(SQLiteDatabase db, long tableId) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE ");
sb.append(getTableName(db, Long.toString(tableId)));
sb.append(" (");
sb.append(Db.ID_COLUMN_NAME);
sb.append(" INTEGER PRIMARY KEY, ");
sb.append(Db.VERSION_COLUMN_NAME);
sb.append(" INTEGER NOT NULL ");
Cursor q = db.rawQuery("select " + Db.Column.NAME_COLUMN + ", " +
Db.Column.TYPE_ID_COLUMN + ", " +
Db.Column.IS_NOT_NULL + " " +
"from " + Db.Column.TABLE_NAME + " " +
"where " + Db.Column.TABLE_ID_COLUMN + " = ?",
new String [] {Long.toString(tableId)});
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
sb.append(", ");
int nameColumn = q.getColumnIndex(Db.Column.NAME_COLUMN);
int typeColumn = q.getColumnIndex(Db.Column.TYPE_ID_COLUMN);
int notNullColumn = q.getColumnIndex(Db.Column.IS_NOT_NULL);
String name = q.getString(nameColumn);
int type = q.getInt(typeColumn);
int notNull = q.getInt(notNullColumn);
sb.append(name);
switch (type) {
case Db.DataType.INTEGER:
sb.append(" INTEGER");
break;
case Db.DataType.TEXT:
sb.append(" TEXT");
break;
}
if (notNull > 0) {
sb.append(" NOT NULL");
}
}
sb.append(");");
db.execSQL(sb.toString());
}
...
}
}
values.put(Db.VERSION_COLUMN_NAME, ver);
db.insert(tableName, "_id", values);
}
public String getTableName(SQLiteDatabase db, String tableId) {
String r = "";
Cursor q = db.rawQuery("select " + Db.Table.NAME_COLUMN + " " +
"from " + Db.Table.TABLE_NAME + " " +
"where " + Db.Table._ID + " = ?",
new String [] {tableId});
if (q.moveToFirst()) {
int nameColumn = q.getColumnIndex(Db.Table.NAME_COLUMN);
r = q.getString(nameColumn);
}
return r;
}
protected void createTable(SQLiteDatabase db, long tableId) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE ");
sb.append(getTableName(db, Long.toString(tableId)));
sb.append(" (");
sb.append(Db.ID_COLUMN_NAME);
sb.append(" INTEGER PRIMARY KEY, ");
sb.append(Db.VERSION_COLUMN_NAME);
sb.append(" INTEGER NOT NULL ");
Cursor q = db.rawQuery("select " + Db.Column.NAME_COLUMN + ", " +
Db.Column.TYPE_ID_COLUMN + ", " +
Db.Column.IS_NOT_NULL + " " +
"from " + Db.Column.TABLE_NAME + " " +
"where " + Db.Column.TABLE_ID_COLUMN + " = ?",
new String [] {Long.toString(tableId)});
for (q.moveToFirst();!q.isAfterLast();q.moveToNext()) {
sb.append(", ");
int nameColumn = q.getColumnIndex(Db.Column.NAME_COLUMN);
int typeColumn = q.getColumnIndex(Db.Column.TYPE_ID_COLUMN);
int notNullColumn = q.getColumnIndex(Db.Column.IS_NOT_NULL);
String name = q.getString(nameColumn);
int type = q.getInt(typeColumn);
int notNull = q.getInt(notNullColumn);
sb.append(name);
switch (type) {
case Db.DataType.INTEGER:
sb.append(" INTEGER");
break;
case Db.DataType.TEXT:
sb.append(" TEXT");
break;
}
if (notNull > 0) {
sb.append(" NOT NULL");
}
}
sb.append(");");
db.execSQL(sb.toString());
}
...
}
}
Реализация API описания метаданных достаточно прямолинейна. Некоторую сложность здесь представляет лишь реализация createTable, представляющая собой решение задачи кодогенерации "в миниатюре", создающая запрос create table и выполняющая его "на лету". Также, не забываем привязывать данные, создаваемые в addData к версии патча.
Таким образом, мы разработали (пока абстрактную) заготовку нашего Content Provider-а. В следующей статье, мы займемся непосредственно кодогенератором, попутно дописав необходимый функционал в Content Provider-е.
Комментариев нет:
Отправить комментарий