AnnotatedSQL lib — автоматическая генерация базы данных в Android

в 11:23, , рубрики: android, annotations, Preprocessor, Разработка под android, метки: , ,

При разработке приложения под Android мы часто пишем руками скрипты для создания схемы.
Все бы ничего когда это надо сделать одни раз, но когда приложение развивается, то часто приходится менять базу.
И когда это размазано по нескольким классам — то возникают проблемы, где-то забыл добавить/удалить колонку, изменить тип и прочее. Еще и копипаст «помогает»: добавлял колонку — забыл поставить запятую.

Как раз для решения этих проблем и была придумана эта библиотека.

AnnotatedSQL — библиотека которая сгенерит код для создания базы данных по аннотациям. Аннотации не runtime, а обрабатываются препроцессором во время компиляции. Тем самым мы никак не афектим проект и конечный apk.

Собственно либа и состоит из двух кусков: jar с аннотациями и препроцессора.
Аннотации кладем в папку libs проекта.
Ну а использование препроцессор зависит от IDE и способа сборки проекта.

Если вы юзаете Eclipse — то копируем плагин в папку plugins и перезапускаем eclipse если надо. далее идем в настройки проекта Java Compiler -> Annotation Processing и выбираем там папку куда генерить код. Очевидно надо поставить стандартную папку gen. Далее идем в Factory Path и выбираем наш плагин. ну вот и все.

Для IDEA плагин не собирал, сори

Для использования с ant — надо просто добавить препроцессор в classpath делаем это примерно так

ant clean release -cp ../com.annotatedsql.AnnotatedSQL_1.0.12.jar

Использование

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

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

public class FStore {

	public static final String DB_NAME = "fmanager";
	public static final int DB_VERSION = 34;

..........

       public static interface TeamTable{
		
		String TABLE_NAME = "team_table";

		String ID = "_id";
		
		String TITLE = "title";
		
		String CHEMP_ID = "chemp_id";
		
		String IS_FAV = "is_fav";
       }

       public static interface ChempTable{

		String TABLE_NAME = "chemp_table";
		
		String CONTENT_PATH = "chemps";

		String ID = "_id";
		
		String TITLE = "title";
       }

       public static interface ResultsTable{

		String CONTENT_PATH = "results";
		
		String PATH_VIEW = "results_view";
		
		String TABLE_NAME = "result_table";

		String ID = "_id";
		
		String TEAM_ID = "team_id";
				
		String POINTS = "points";
		
		String CHEMP_ID = "chemp_id";
		
		String GAMES = "games";
		
		String WINS = "wins";

		String TIE = "tie";
		
		String LOSE = "lose";
		
		String BALLS = "balls";
		
		String GOALS = "goals";
		
       }

........
}

Пока не обращаем внимание на CONTENT_PATH и всякие PATH_VIEW. Это константы для доступа в контент провайдер.
Итак, мы представляем объем ручной работы для создания схемы.
В добавок, что бы получить результат в читаемой форме нам надо заджойнить таблички друг на друга. Это можно сделать в контент провайдере, но я предпочитаю юзать View, вот еще большой sql кусок для написания.

Для облегчения нашей работы и была написана эта либа. Итак приступим.

Schema

FStore — помечаем аннотацией Schema(«SqlSchema») и задаем имя класса который будет содержать код. класс будет сгенерен в том же пакете, где лежит FStore


@Schema("SqlSchema")
public class FStore {

Table, Index, PrimaryKey

Описание табличек помечаем аннотацией Table и задаем имя таблицы


@Table(ChempTable.TABLE_NAME)
public static interface ChempTable{

................

@Table(TeamTable.TABLE_NAME)
public static interface TeamTable{

...............

@Table(ResultsTable.TABLE_NAME)
@Index(name = "chemp_index", columns = ResultsTable.CHEMP_ID)
@PrimaryKey(collumns = {ResultsTable.TEAM_ID, ResultsTable.CHEMP_ID})
public static interface ResultsTable{

Как видим на таблицу мы можем повесить создание индекса, и сложного ключа. Тут вроде все просто и не требует объяснения

Column, PrimaryKey, Autoincrement, NotNull

Эти аннотации предназначены для полей, и очевидны в использовании тоже

	@Table(TeamTable.TABLE_NAME)
	public static interface TeamTable{
		
		String TABLE_NAME = "team_table";

		@PrimaryKey
		@Column(type = Type.INTEGER)
		String ID = "_id";
		
		@NotNull
		@Column(type = Type.TEXT)
		String TITLE = "title";
		
		@Column(type = Type.INTEGER)
		@NotNull
		String CHEMP_ID = "chemp_id";
		
		@Column(type = Type.INTEGER, defVal="0")
		String IS_FAV = "is_fav";
	}
SimpleView

И последний, очень важный, элемент системы и не совсем тривиальный это SimpleView.
Он предоставляет базовый функционал для создания простых вьюх. Тут пока есть INNER JOIN, но я добавлю и другие.

	@SimpleView(ResultView.VIEW_NAME)
	public static interface ResultView{
		
		String VIEW_NAME = "result_view";
		
		@From(ResultsTable.TABLE_NAME)
		String TABLE_RESULT = "table_result";
		
		@Join(srcTable = TeamTable.TABLE_NAME, srcColumn = TeamTable.ID, destTable = ResultView.TABLE_RESULT, destColumn = ResultsTable.TEAM_ID)
		String TABLE_TEAM = "table_team";
		
		@Join(srcTable = ChempTable.TABLE_NAME, srcColumn = ChempTable.ID, destTable = ResultView.TABLE_RESULT, destColumn = ResultsTable.CHEMP_ID)
		String TABLE_CHEMP = "table_chemp";
	}

Рассмотрим аннотации внутри нашей вьюхи:
From — это табличка из которой будем делать from :) Важно — далее при джойнах надо использовать не имя таблицы, а именно эту константу.

Join — собственно таблицы джойна. В нашем случае надо заджойнится на таблицу команды и чемпионата.

srcTable — это исходная таблица.
destTable — это новое название таблицы from/join во вьюхе. В нашем случае

String TABLE_RESULT = "table_result";

Еще очень важное замечание — во вьюхе имена полей генерятся по следующему паттерну:
<variable_name>_<column_name>

Исключение — поле _id из таблицы From, что бы юзать cursor в адаптере.

Следовательно, что бы найти индекс колонки надо юзаьть нечто вроде

columnPoints = cursor.getColumnIndex(ResultView.TABLE_RESULT + "_" + ResultsTable.POINTS);

немного неудобно, но это делается один раз в

public void changeCursor(Cursor cursor) {

можно еще заюзать такой хелпер

public class ColumnMappingHelper {

	private HashMap<String, HashMap<String, Integer>> indexes = new HashMap<String, HashMap<String, Integer>>();
	
	public int getColumn(Cursor c, String table, String column){
		HashMap<String, Integer> columns = indexes.get(table);
		if(columns != null){
			Integer index = columns.get(column);
			if(index != null)
				return index;
		}
		
		if(columns == null){
			columns = new HashMap<String, Integer>();
			indexes.put(table, columns);
		}
		int index = c.getColumnIndex(table + "_" + column);
		columns.put(column, index);
		return index;
	}
}

и юзаем его так

mappingHelper.getColumn(cursor, ResultView.TABLE_RESULT, ResultsTable.POINTS);
Результат

Сгенеренный файлик SqlSchema.java

public class SqlSchema{
	 
	 private static final String SQL_CREATE_RESULT_TABLE = "create table result_table( balls INTEGER, chemp_id INTEGER NOT NULL, games INTEGER NOT NULL, goals INTEGER, _id INTEGER, lose INTEGER, points INTEGER NOT NULL, team_id INTEGER NOT NULL, tie INTEGER, wins INTEGER, PRIMARY KEY( team_id, chemp_id))";
	 
	 private static final String SQL_CREATE_CHEMP_TABLE = "create table chemp_table( _id INTEGER PRIMARY KEY, title TEXT)";
	 
	 private static final String SQL_CREATE_TEAM_TABLE = "create table team_table( chemp_id INTEGER NOT NULL, _id INTEGER PRIMARY KEY, is_fav INTEGER DEFAULT (0), title TEXT NOT NULL)";
	 

	 private static final String SQL_CREATE_CHEMP_INDEX = "create index idx_chemp_index on result_table( chemp_id)";
	 
	 
	 private static final String SQL_CREATE_RESULT_VIEW = "CREATE VIEW result_view AS SELECT   table_chemp._id as table_chemp__id, table_chemp.title as table_chemp_title, table_result.balls as table_result_balls, table_result.chemp_id as table_result_chemp_id, table_result.games as table_result_games, table_result.goals as table_result_goals, table_result._id, table_result.lose as table_result_lose, table_result.points as table_result_points, table_result.team_id as table_result_team_id, table_result.tie as table_result_tie, table_result.wins as table_result_wins, table_team.chemp_id as table_team_chemp_id, table_team._id as table_team__id, table_team.is_fav as table_team_is_fav, table_team.title as table_team_title FROM result_table AS table_result JOIN chemp_table AS table_chemp ON table_chemp._id = table_result.chemp_id JOIN team_table AS table_team ON table_team._id = table_result.team_id";
	 

	 public static void onCreate(final SQLiteDatabase db) {
		db.execSQL(SQL_CREATE_RESULT_TABLE);
		db.execSQL(SQL_CREATE_CHEMP_TABLE);
		db.execSQL(SQL_CREATE_TEAM_TABLE);
		db.execSQL(SQL_CREATE_SCORE_TABLE);
		
		db.execSQL(SQL_CREATE_CHEMP_INDEX);
		
		db.execSQL(SQL_CREATE_RESULT_VIEW);
		db.execSQL(SQL_CREATE_SCORE_VIEW);
	}
	
	public static void onDrop(final SQLiteDatabase db){
		db.execSQL("drop table if exists result_table");
		db.execSQL("drop table if exists chemp_table");
		db.execSQL("drop table if exists team_table");
		db.execSQL("drop table if exists score_table");
		
		db.execSQL("drop view if exists result_view");
		db.execSQL("drop view if exists score_view");
	}
}

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

Использование SqlSchema
	private class AnnotationSql extends SQLiteOpenHelper {

		public AnnotationSql(Context context) {
			super(context, FStore.DB_NAME, null, FStore.DB_VERSION);
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			SqlSchema.onCreate(db);
			init(db);
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
			SqlSchema.onDrop(db);
			onCreate(db);
		}

	}
Планы

1. Добавить разные типы джойнов
2. Добавить аннотацию Columns для джойна, что бы выгребать только нужные поля

Ссылки

Бинарники: github.com/hamsterksu/Android-AnnotatedSQL-binaries
Исходники: github.com/hamsterksu/Android-AnnotatedSQL

Лицензия: MIT

Всем спасибо!

Автор: hamsterksu


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js