Лучший 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/
Добавить комментарий