Pull to refresh

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

Reading time5 min
Views6.3K

Лучший 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(mode = LazyThreadSafetyMode.NONE) {
        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 = "&")
    }
}

UPD: добавил в lazy-инициализацию mode = LazyThreadSafetyMode.NONE, спасибо konstantin_berkow


Вместо заключения


Как оказалось, настройка jOOQ в Android – не такой уж и сложный процесс. Достаточно проделать его один раз, а далее можно смело заниматься копипастом из старых проектов.


И небольшой бонус, который дает jOOQ тем, кто его использует. Как видно из примера, пи открытии подключения используется cached mode. В чем же цимес? Android SDK SQLite API не предоставляет возможности работать с БД в данном режиме, сильно ограничивая нас в организации межпроцессного взаимодействия в приложениях. Теперь же – можно смело использовать данный режим, что уже само по себе может послужить причиной перехода на этот замечательный фреймворк.

Only registered users can participate in poll. Log in, please.
Стоит ли овчинка выделки?
22.73% Да10
77.27% Нет, больной ты ублюдок34
44 users voted. 31 users abstained.
Tags:
Hubs:
+13
Comments2

Articles