При разработке приложения под 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

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

Update #1: насчет обновления схемы

OpenHelper я не генерирую, вы его пишите сами. так что никто не мешает написать там сложную логику, а генеренный скрипт будет работать для onCreate.
В onUpgrade можно написать добавление/удаление/изменение полей совсем просто — имена таблиц и полей у вас есть.
В схеме я сделаю мемберы открытыми — тогда вы сможете получить доступ к ним и после изменения таблиц — пересоздать вьюшки