Многие разработчики, используют в своих проектах библиотеку Retrofit, которая позволяет превратить HTTP API в java-интерфейс. Это очень удобно, так как позволяет избавиться от лишнего кода и использовать ее очень легко. Нужно лишь создать интерфейс и навесить несколько аннотаций.
Недавно я занимался разработкой приложения для Android, которому необходимо было делать запросы к базе данных через JDBC- драйвер. Тогда мне пришла идея создать нечто подобное Retrofit только для запросов к базе данных. Так появился RetroBase, о котором я Вам сейчас и расскажу.
This article in English. Эта статья на английском.
Для того, чтобы интерфейс и аннотации превратились в рабочий код, потребуется Annotation Processing, который открывает поистине огромные возможности для автоматизации написания однотипного кода. А в сочетании с JavaPoet процесс генерации java-кода становится удобным и простым.
На хабре, как и на просторах интернета, имеется несколько хороших статей по этой теме, поэтому разобраться с Annotation Processing не составляет труда, а необходимый мануал библиотеки JavaPoet умещается в ее README.md.
Основу RetroBase составляют две аннотации
Самое интересное происходит в
После этого создается соединение с БД:
Также создается
… и реализация метода для этого запроса:
При этом учитывается тип возвращаемого значения метода. Он может быть либо
Также выполняется проверка на то, может ли метод выбрасывать
Все параметры аннотированного метода добавляются в переопределяющий метод, а также для каждого параметра генерируется выражение позволяющее передать значение параметра в
Конечно же, количество и типы параметров метода должны соответствовать параметрам запроса, переданного с помощью аннотации
После того, как файл был сгенерирован, он записывается средствами Annotation Processing:
Конечно, удобно получать
Для этого был создан
Все, что нужно — это добавить к методу аннотацию
Например, для методов интерфейса
Здесь
Как видно из сгенерированного метода, нам потребуется создание объектов класса модели из
Естественно, что параметры сгенерированного метода будут точно соответствовать параметрам метода, вызов которого происходит:
Все исключения, которые происходят при выполнении запроса, передаются Subscriber'у как и положено в Rx.
Пример использования всего описанного выше может выглядеть следующим образом:
А если нужно подменить
На github Вы можете найти исходники с комментариями, а также рабочий пример этой небольшой библиотеки.
Целью этой статьи было показать насколько полезным может быть Annotation Processing, позволяющий избавиться от написания однотипного кода. И, надеюсь, у Вас могут появиться новые идеи по использованию этого инструмента в своих проектах.
UPD. 1: благодаря замечаниям в комментариях была добавлена проверка отписки подписчика в RX-методах-обертках. (Версия RetroBase 1.0.4)
UPD. 2: если Вам необходимо выполнить запрос INSERT и получить id созданной записи, получить количество строк, измененных в результате выполнения запроса UPDATE или узнать id записей, которые были удалены запросом DELETE, Вы можете добавить конструкцию
Как видно из кода, возможно также использовать аннотацию
UPD. 3: в версии RertoBase 1.1 добавлена проверка
UPD. 4: в версии RertoBase 1.2 весь Rx код генерируется для версии RxJava 2.x. Пример сгенерированных методов Вы видели выше. Также таким образом была решена проблема backpressure, о которой было указано в комментариях.
Недавно я занимался разработкой приложения для Android, которому необходимо было делать запросы к базе данных через JDBC- драйвер. Тогда мне пришла идея создать нечто подобное Retrofit только для запросов к базе данных. Так появился RetroBase, о котором я Вам сейчас и расскажу.
This article in English. Эта статья на английском.
Для того, чтобы интерфейс и аннотации превратились в рабочий код, потребуется Annotation Processing, который открывает поистине огромные возможности для автоматизации написания однотипного кода. А в сочетании с JavaPoet процесс генерации java-кода становится удобным и простым.
На хабре, как и на просторах интернета, имеется несколько хороших статей по этой теме, поэтому разобраться с Annotation Processing не составляет труда, а необходимый мануал библиотеки JavaPoet умещается в ее README.md.
Основу RetroBase составляют две аннотации
DBInterface
и DBQuery
вместе с DBAnnotationProcessor
, который и выполняет всю работу. С помощью DBInterface
отмечается интерфейс с методами-запросами к БД, а DBQuery
отмечает сами методы. Методы могут иметь параметры, которые будут использованы в SQL-запросе. Например:@DBInterface(url = SpendDB.URL, login = SpendDB.USER_NAME, password = SpendDB.PASSWORD)
@DBInterfaceRx
public interface SpendDB {
String USER_NAME = "postgres";
String PASSWORD = "1234";
String URL = "jdbc:postgresql://192.168.1.26:5432/spend";
@DBMakeRx(modelClassName = "com.qwert2603.retrobase_example.DataBaseRecord")
@DBQuery("SELECT * from spend_test")
ResultSet getAllRecords();
@DBMakeRx
@DBQuery("DELETE FROM spend_test WHERE id = ?")
void deleteRecord(int id) throws SQLException;
}
Самое интересное происходит в
DBAnnotationProcessor
, где осуществляется генерация класса, реализующего интерфейс, сгенерированный класс будет иметь имя *название_интерфейса* + Impl
:TypeSpec.Builder newTypeBuilder = TypeSpec.classBuilder(dbInterfaceClass.getSimpleName() + GENERATED_FILENAME_SUFFIX)
.addSuperinterface(TypeName.get(dbInterfaceClass.asType()))
.addField(mConnection)
.addMethod(waitInit)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
После этого создается соединение с БД:
FieldSpec mConnection = FieldSpec.builder(Connection.class, "mConnection", Modifier.PRIVATE)
.initializer("null")
.build();
Также создается
PreparedStatement
для каждого запроса:FieldSpec fieldSpec = FieldSpec
.builder(PreparedStatement.class, executableElement.getSimpleName().toString() + PREPARED_STATEMENT_SUFFIX)
.addModifiers(Modifier.PRIVATE)
.initializer("null")
.build();
… и реализация метода для этого запроса:
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(executableElement.getSimpleName().toString())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(returnTypeName);
При этом учитывается тип возвращаемого значения метода. Он может быть либо
void
, если SQL-запрос представляет собой INSERT, DELETE или UPDATE. Либо ResultSet
, если SQL-запрос представляет собой SELECT.Также выполняется проверка на то, может ли метод выбрасывать
SQLException
. Если может, они будут выброшены и из реализации метода. А если нет — пойманы и выведены в stderr
.Все параметры аннотированного метода добавляются в переопределяющий метод, а также для каждого параметра генерируется выражение позволяющее передать значение параметра в
PreparedStatement
:insertRecord_PreparedStatement.setString(1, kind);
Конечно же, количество и типы параметров метода должны соответствовать параметрам запроса, переданного с помощью аннотации
DBQuery
.После того, как файл был сгенерирован, он записывается средствами Annotation Processing:
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(filename);
Writer writer = sourceFile.openWriter();
writer.write(javaFile.toString());
Rx it!
Конечно, удобно получать
ResultSet
, определяя лишь интерфейс. А еще удобнее было бы воспользоваться популярной RxJava и получать Observable
. К тому же, это позволит легко решить проблему с выполнением запросов в другом потоке.Для этого был создан
DBMakeRxAnnotationProcessor
вместе с DBInterfaceRx
и DBMakeRx
, которые позволяют создать класс с методами-обертками. Применение этих аннотаций Вы уже могли увидеть в примере выше. Созданный класс будет иметь имя *название_интерфейса* + Rx
, а также будет иметь открытый конструктор, принимающий объект интерфейса, аннотированного DBInterfaceRx
, которому он будет перенаправлять запросы, возвращая результаты в реактивном стиле.Все, что нужно — это добавить к методу аннотацию
DBMakeRx
и передать ей название класса модели. Сгенерированный метод-обертка будет возвращать Observable<*класс модели*>
. При этом, название класса модели можно и не определять. В этом случае сгенерированный метод будет возвращать io.reactivex.Completable
, что удобно для SQL-запросов INSERT, DELETE или UPDATE, для которых не требуется возвращение результата.Например, для методов интерфейса
ResultSet getAllRecords();
и void deleteRecord(int id) throws SQLException;
из примера выше будет сгенерированы следующие методы-обертки: public io.reactivex.Observable<com.qwert2603.retrobase_example.DataBaseRecord> getAllRecords() {
return Observable.generate(() -> mDB.getAllRecords(), (resultSet, objectEmitter) -> {
if (resultSet.next()) {
objectEmitter.onNext(new com.qwert2603.retrobase_example.DataBaseRecord(resultSet));
}
else {
objectEmitter.onComplete();
}
} , ResultSet::close);
}
public Completable deleteRecord(int id) {
return Completable.fromAction(() -> mDB.deleteRecord(id));
}
Здесь
mDB
представляет собой объект интерфейса, аннотированного DBInterfaceRx
, который был передан в конструктор.Как видно из сгенерированного метода, нам потребуется создание объектов класса модели из
ResultSet
, поэтому у класса модели должен быть открытый конструктор, который принимает ResultSet
.Естественно, что параметры сгенерированного метода будут точно соответствовать параметрам метода, вызов которого происходит:
public Completable insertRecord(String kind, int value, Date date) {
...
mDB.insertRecord(kind, value, date);
...
}
Все исключения, которые происходят при выполнении запроса, передаются Subscriber'у как и положено в Rx.
Пример использования всего описанного выше может выглядеть следующим образом:
private SpendDB mSpendDB = new SpendDBImpl();
private SpendDBRx mSpendDBRx = new SpendDBRx(mSpendDB);
public Single<List<DataBaseRecord>> getAllRecords() {
return mSpendDBRx.getAllRecords()
.toList()
.compose(applySchedulers());
}
А если нужно подменить
new SpendDBImpl();
или new SpendDBRx(mSpendDB);
для выполнения тестов, можно воспользоваться популярным Dagger.На github Вы можете найти исходники с комментариями, а также рабочий пример этой небольшой библиотеки.
Целью этой статьи было показать насколько полезным может быть Annotation Processing, позволяющий избавиться от написания однотипного кода. И, надеюсь, у Вас могут появиться новые идеи по использованию этого инструмента в своих проектах.
UPD. 1: благодаря замечаниям в комментариях была добавлена проверка отписки подписчика в RX-методах-обертках. (Версия RetroBase 1.0.4)
UPD. 2: если Вам необходимо выполнить запрос INSERT и получить id созданной записи, получить количество строк, измененных в результате выполнения запроса UPDATE или узнать id записей, которые были удалены запросом DELETE, Вы можете добавить конструкцию
returning id
в конец SQL запроса и получить ResultSet
c искомыми id. @DBMakeRx(modelClassName = "com.qwert2603.spenddemo.model.Id")
@DBQuery("UPDATE spend_test SET kind=?, value=?, date=? WHERE id=? returning id")
ResultSet updateRecord(String kind, int value, Date date, int id) throws SQLException;
Как видно из кода, возможно также использовать аннотацию
DBMakeRx
, чтобы получать Observable<*id*>
. Для нужно создать класс модели, получающий Id из ResultSet
и передать его параметру modelClassName
аннотации DBMakeRx
. Класс модели, содержащий Id может выглядеть следующим образом:public class Id {
private int mId;
public Id(ResultSet resultSet) throws SQLException {
mId = resultSet.getInt(1);
}
public int getId() {
return mId;
}
public void setId(int id) {
mId = id;
}
}
UPD. 3: в версии RertoBase 1.1 добавлена проверка
java.sql.Connection#isValid(0)
для автоматического создания нового подключения к БД в случае ошибки (например, потеря соединения).UPD. 4: в версии RertoBase 1.2 весь Rx код генерируется для версии RxJava 2.x. Пример сгенерированных методов Вы видели выше. Также таким образом была решена проблема backpressure, о которой было указано в комментариях.