Лучший SQL Builder. Используем jOOQ на Android
Введение
При разработке Android-приложений вполне естественным считается использовать SQLite базу данных в качестве основного хранилища. Обычно, базы данных на мобильных устройствах имеют весьма простенькие схемы и состоят из 10-15 таблиц. Для подобных случаев подходит почти любой SQL Builder, ORM, и даже голый SQLite API.
Но, увы, не всем разработчикам везет, и порой на нашу долю выпадает описывать большие модели данных, использовать хранимые процедуры, настраивать работу с кастомными типами данных или писать 10 INNER JOIN в запросе за очень толстой сущностью. Так не повезло и вашему покорному слуге, из чего и появился материал для данной статьи. Что же, суровые времена требуют суровых мер. Итак, накатываем jOOQ на Android.
Все бы хорошо, но
Но есть два факта, с которыми нужно будет совладать. Первый из них подстерегает нас на самом начале работы с jOOQ: на этапе идеологическом. Для того, чтобы инициировать процесс кодогенерации, нужно, собственно, заиметь базу данных, к которой jooq plugin подключится. Данная проблема решается легко, создаем template-проект с описанием gradle task для генерации, после чего создаем БД локально, прописываем в конфигах пути, запускаем плагин и копируем полученные исходники к себе в проект.
Далее, допустим мы сгенерировали все необходимые классы. Просто так скопировать их в Android-проект мы не сможем – будут требоваться дополнительные зависимости, первая из которых – на javax аннотации. Варианта два, оба банальные. Либо добавляем библиотеку (org.glassfish:javax.annotation), либо – используем замечательный инструмент – find & replace in scope.
И вот казалось бы, все хорошо, все предварительные настройки сделаны, классы скопированы и импортированы в проект. Возможно вам даже удастся запустить приложение, и есть шанс, что оно заработает. Если вы обязаны поддерживать Android API Level < 24 – не ведитесь, на это наш путь еще не заканчивается. Дело заключается в том, что jOOQ на текущий момент в open-source версии во многом использует Java 8, которая, как известно, с Android дружит весьма условно. Эта проблема также решается двумя вариантами: либо покупаем jOOQ, пишем в саппорт и слезно выпрашиваем версию на Java 6 или Java 7 (у них есть, судя по статьям в сети), либо же, если у вас, как и у меня, нет жесткой необходимости обладать всеми последними нововведениями библиотеки, равно как и желания платить, то есть второй путь. jOOQ начал переходить на Java 8 не так давно. Последняя из версий до миграции является 3.6.0, что значит, что мы можем использовать генератор с параметром groovy version = '3.6.0'
и поддерживать старые версии устройств.
И последнее, что ждет энтузиастов, пошедших по этой тропинке отчаяния. В Android в принципе нет JDBC, что значит, что пришло время скрестив пальцы искать 3rd-party solutions. К счастью, подобная библиотека есть – SQLDroid.
Все. Основные этапы и действия на них бегло расписаны. Теперь перейдем к коду, тут все в целом довольно логично, но, дабы сократить ваше время, приведу примеры из собственного проекта.
Кодогенерация
Настройка jOOQ плагина будет выглядеть следующим образом:
buildScript {
repositories {
mavenCentral()
}
dependencies {
classpath "nu.studer:gradle-jooq-plugin:$jooq_plugin_version"
}
}
apply plugin: 'nu.studer.jooq'
dependencies {
jooqRuntime "org.xerial:sqlite-jdbc:$xerial_version"
}
jooq {
version = '3.6.0'
edition = 'OSS'
dev(sourceSets.main) {
jdbc {
driver = 'org.sqlite.JDBC'
url = 'jdbc:sqlite:/Path/To/Database/database.db3'
}
generator {
name = 'org.jooq.util.DefaultGenerator'
strategy {
name = 'org.jooq.util.DefaultGeneratorStrategy'
}
database {
name = 'org.jooq.util.sqlite.SQLiteDatabase'
}
generate {
relations = true
deprecated = false
records = true
immutablePojos = true
fluentSetters = true
}
target {
packageName = 'com.example.mypackage.data.database'
}
}
}
}
Android
Необходимые зависимости:
implementation "org.jooq:jooq:$jooq_version"
implementation "org.sqldroid:sqldroid:$sqldroid_version"
implementation "org.glassfish:javax.annotation:$javax_annotations_version"
А теперь исходники класса-обертки, для работы с jOOQ через SQLiteOpenHelper. В целом, без него можно было бы обойтись, но так куда удобнее (на мой взгляд), чтобы благополучно пользоваться и одним, и вторым API.
class DatabaseAdapter(private val context: Context)
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "database"
private const val DATABASE_VERSION = 1
@JvmStatic private val OPEN_OPTIONS = mapOf(
"cache" to "shared",
"journal_mode" to "WAL",
"synchronous" to "ON",
"foreign_keys" to "ON")
}
val connectionLock: ReentrantLock = ReentrantLock(true)
val configuration: Configuration by lazy {
connectionLock.withLock {
// ensure the database exists,
// all upgrades are performed,
// and connection is ready to be set
val database = context.openOrCreateDatabase(
DATABASE_NAME,
Context.MODE_PRIVATE,
null)
if (database.isOpen) {
database.close()
}
// register SQLDroid driver to be used for establishing connections
// with our database
DriverManager.registerDriver(
Class.forName("org.sqldroid.SQLDroidDriver")
.newInstance() as Driver)
DefaultConfiguration()
.set(SQLiteSource(
context,
OPEN_OPTIONS,
"database",
arrayOf("databases")))
.set(SQLDialect.SQLITE)
}
}
override fun onCreate(db: SQLiteDatabase) {
// acquire monitor until the database connection is created
// this is important as otherwise transactions might be tryingg to run
// concurrently that will lead to crashes
connectionLock.withLock {
// TODO: Create tables
}
}
override fun onOpen(db: SQLiteDatabase) {
// acquire monitor until the database connection is established
// this is important as otherwise transactions might be tryingg to run
// concurrently that will lead to crashes
connectionLock.withLock {
super.onOpen(db)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// acquire monitor until the database is upgraded
// this is important as otherwise transactions might be tryingg to run
// concurrently that will lead to crashes
connectionLock.withLock {
}
}
infix inline fun <reified T> transaction(noinline f: (Configuration) -> T): Observable<T>
= Observable.create { emitter ->
val tryResult = Try {
connectionLock.withLock {
DSL.using(configuration).transactionResult(f)
}
}
when (tryResult) {
is Try.Success -> {
emitter.onNext(tryResult.value)
emitter.onComplete()
}
is Try.Failure -> {
emitter.onError(tryResult.exception)
}
}
}
fun invalidate() {
connectionLock.withLock {
// TODO: Drop tables, vacuum and create tables
}
}
private class SQLiteSource(val context: Context,
val options: Map<String, String>,
val database: String,
val fragments: Array<out String>): DroidDataSource() {
override fun getConnection(): Connection
= openConnection(options)
private fun openConnection(options: Map<String, String> = emptyMap()): Connection {
return DriverManager.getConnection(StringBuilder().apply {
append("jdbc:sqldroid:")
append(context.applicationInfo.dataDir)
append("/")
append(buildFragments(fragments))
append(database)
append("?")
append(buildOptions(options))
}.toString())
}
private fun buildFragments(fragments: Array<out String>)
= when (fragments.isEmpty()) {
true -> ""
false -> "${fragments.joinToString("/")}/"
}
private fun buildOptions(options: Map<String, String>)
= options.mapTo(mutableListOf<String>()) { entry ->
"${entry.key}=${entry.value}"
}
.joinToString(separator = "&")
}
}
Вместо заключения
Как оказалось, настройка jOOQ в Android – не такой уж и сложный процесс. Достаточно проделать его один раз, а далее можно смело заниматься копипастом из старых проектов.
И небольшой бонус, который дает jOOQ тем, кто его использует. Как видно из примера, пи открытии подключения используется cached mode. В чем же цимес? Android SDK SQLite API не предоставляет возможности работать с БД в данном режиме, сильно ограничивая нас в организации межпроцессного взаимодействия в приложениях. Теперь же – можно смело использовать данный режим, что уже само по себе может послужить причиной перехода на этот замечательный фреймворк.
Автор: KomarovI