Лучший SQL Builder – используем jOOQ на Android

от автора

Лучший 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.sqldroid:sqldroid:$sqldroid_version" implementation "org.jooq:jooq:$jooq_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 = "&")     } }


ссылка на оригинал статьи https://habr.com/post/422303/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *