RetroBase — аналог Retrofit для запитів до баз даних

Багато розробники, що використовують у своїх проектах бібліотеку Retrofit, яка дозволяє перетворити HTTP API в java-інтерфейс. Це дуже зручно, оскільки дозволяє позбавитися від зайвого коду і використовувати її дуже легко. Потрібно лише створити інтерфейс і навісити кілька анотацій.

Нещодавно я займався розробкою програми для Android, якому необхідно було робити запити до бази даних через JDBC — драйвер. Тоді мені прийшла ідея створити щось подібне Retrofit тільки для запитів до бази даних. Так з'явився RetroBase, про який я Вам зараз і розповім.

Для того, щоб інтерфейс і анотації перетворилися в робочий код, потрібно 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 і отримувати
Вами
. До того ж, це дозволить легко вирішити проблему з виконанням запитів в іншому потоці.

Для цього був створений
DBMakeRxAnnotationProcessor
разом
DBInterfaceRx
та
DBMakeRx
, які дозволяють створити клас з методами-обгортками. Застосування цих анотацій Ви вже могли побачити в прикладі вище. Створений клас буде мати ім'я
*название_интерфейса* + Rx
, а також буде мати відкритий конструктор, що приймає об'єкт інтерфейсу, анотованого
DBInterfaceRx
, яким він буде перенаправляти запити, повертаючи результати в реактивному стилі.

Все, що потрібно — це додати до методу анотацію
DBMakeRx
і передати їй назву класу моделі. Згенерований метод-обгортка буде повертати
Вами<*клас моделі*>
. При цьому, назва класу моделі можна і не визначати. У цьому випадку згенерований метод буде повертати
Вами<java.lang.Object>
, що зручно для SQL-запитів INSERT, DELETE або UPDATE, для яких не вимагається повернення результату.

Наприклад, для методу інтерфейсу
ResultSet getAllRecords();
з прикладу вище буде згенерований наступний метод-обгортка:
public rx.Вами<com.qwert2603.retrobase_example.DataBaseRecord> getAllRecords() {
return Вами.create(subscriber -> {
try {
ResultSet resultSet = mDB.getAllRecords();
while (resultSet.next()) {
subscriber.onNext(new com.qwert2603.retrobase_example.DataBaseRecord(resultSet));
}
subscriber.onCompleted();
}
catch (Exception e) {
subscriber.onError(e);
}
} );
}

Тут
mDB
представляє собою об'єкт інтерфейсу, анотованого
DBInterfaceRx
, який був переданий в конструктор.

Як видно з згенерованого методу, нам буде потрібно створення об'єктів класу моделі з
ResultSet
, тому в класу моделі повинен бути відкритий конструктор, який бере
ResultSet
.

Природно, що параметри згенерованого методу будуть точно відповідати параметрам методу, виклик якого відбувається:
public rx.Вами<Object> 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 Вами<List<DataBaseRecord>> getAllRecords() {
return mSpendDBRx.getAllRecords()
.toList()
.compose(applySchedulers());
}

А якщо потрібно підмінити
new SpendDBImpl();
або
new SpendDBRx(mSpendDB);
для виконання тестів, можна скористатися популярним Dagger.

На github Ви можете знайти на исходники з коментарями, а також робочий приклад цієї невеличкої бібліотеки.

Метою цієї статті було показати, наскільки корисним може бути Annotation Processing, що дозволяє позбавитися від написання однотипного коду. І, сподіваюся, у Вас можуть з'явитися нові ідеї щодо використання цього інструменту в своїх проектах.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.