Skip to content

Instantly share code, notes, and snippets.

@InsanusMokrassar
Last active November 25, 2017 10:04
Show Gist options
  • Save InsanusMokrassar/36938477424d8503df94eac06c005c9d to your computer and use it in GitHub Desktop.
Save InsanusMokrassar/36938477424d8503df94eac06c005c9d to your computer and use it in GitHub Desktop.
Very simple ORM CRUD Android SQL managment
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.util.Log
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
val nativeTypesMap = mapOf(
Pair(
Int::class,
"INTEGER"
),
Pair(
Long::class,
"LONG"
),
Pair(
Float::class,
"FLOAT"
),
Pair(
Double::class,
"DOUBLE"
),
Pair(
String::class,
"TEXT"
),
Pair(
Boolean::class,
"BOOLEAN"
)
)
internal fun KClass<*>.tableName(): String {
return java.simpleName
}
fun Map<KProperty<*>, Any>.toContentValues(): ContentValues {
val cv = ContentValues()
keys.forEach {
val prop = it
val value = get(prop)!!
when(value::class) {
Boolean::class -> cv.put(prop.name, value as Boolean)
Int::class -> cv.put(prop.name, value as Int)
Long::class -> cv.put(prop.name, value as Long)
Float::class -> cv.put(prop.name, value as Float)
Double::class -> cv.put(prop.name, value as Double)
Byte::class -> cv.put(prop.name, value as Byte)
ByteArray::class -> cv.put(prop.name, value as ByteArray)
String::class -> cv.put(prop.name, value as String)
Short::class -> cv.put(prop.name, value as Short)
}
}
return cv
}
fun Any.toContentValues(): ContentValues {
return toValuesMap().toContentValues()
}
fun KClass<*>.getVariablesMap(): Map<String, KProperty<*>> {
val futureMap = LinkedHashMap<String, KProperty<*>>()
this.getVariables().forEach {
futureMap.put(it.name, it)
}
return futureMap
}
fun Any.toValuesMap() : Map<KProperty<*>, Any> {
val values = HashMap<KProperty<*>, Any>()
this::class.getVariablesMap().values.filter {
it.intsanceKClass() != Any::class && (!it.returnType.isMarkedNullable || it.call(this) != null)
}.forEach {
it.call(this)?.let { value ->
values.put(
it,
value
)
}
}
return values
}
fun Any.getPrimaryFieldsSearchQuery(): String {
return toValuesMap().filter {
it.key.isPrimaryField()
}.map {
"${it.key.name}=${it.value}"
}.joinToString(
" AND "
)
}
fun <M: Any> Collection<M>.getPrimaryFieldsSearchQuery(): String {
return joinToString(") OR (", "(", ")") {
it.getPrimaryFieldsSearchQuery()
}
}
fun <M: Any> KClass<M>.fromValuesMap(values : Map<KProperty<*>, Any?>): M {
if (constructors.isEmpty()) {
throw IllegalStateException("For some of reason, can't create correct realisation of model")
} else {
val resultModelConstructor = constructors.first {
it.parameters.size == values.size
}
val paramsList = ArrayList<Any?>()
resultModelConstructor.parameters.forEach {
parameter ->
val key = values.keys.firstOrNull { parameter.name == it.name }
key ?. let {
paramsList.add(
values[key]
)
} ?: paramsList.add(null)
}
return resultModelConstructor.call(*paramsList.toTypedArray())
}
}
fun <M: Any> Cursor.extract(modelClass: KClass<M>): M {
val properties = modelClass.getVariablesMap()
val values = HashMap<KProperty<*>, Any?>()
properties.values.forEach {
val columnIndex = getColumnIndex(it.name)
val value: Any = when(it.returnClass()) {
Boolean::class -> getInt(columnIndex) == 1
Int::class -> getInt(columnIndex)
Long::class -> getLong(columnIndex)
Float::class -> getFloat(columnIndex)
Double::class -> getDouble(columnIndex)
Byte::class -> getInt(columnIndex)
ByteArray::class -> getInt(columnIndex)
Short::class -> getShort(columnIndex)
else -> getString(columnIndex)
}
values.put(
it,
value
)
}
return modelClass.fromValuesMap(values)
}
fun <M: Any> Cursor.extractAll(modelClass: KClass<M>, close: Boolean = true): List<M> {
val result = ArrayList<M>()
if (moveToFirst()) {
do {
result.add(extract(modelClass))
} while (moveToNext())
}
if (close) {
close()
}
return result
}
fun <M : Any> SQLiteDatabase.createTableIfNotExist(modelClass: KClass<M>) {
val fieldsBuilder = StringBuilder()
modelClass.getVariables().forEach {
if (it.isReturnNative()) {
fieldsBuilder.append("${it.name} ${nativeTypesMap[it.returnClass()]}")
if (it.isPrimaryField()) {
fieldsBuilder.append(" PRIMARY KEY")
}
if (!it.isNullable()) {
fieldsBuilder.append(" NOT NULL")
}
if (it.isAutoincrement()) {
fieldsBuilder.append(" AUTOINCREMENT")
}
} else {
TODO()
}
fieldsBuilder.append(", ")
}
try {
execSQL("CREATE TABLE IF NOT EXISTS ${modelClass.tableName()} " +
"(${fieldsBuilder.replace(Regex(", $"), "")});")
Log.i("createTableIfNotExist", "Table ${modelClass.tableName()} was created")
} catch (e: Exception) {
Log.e("createTableIfNotExist", "init", e)
throw IllegalArgumentException("Can't create table ${modelClass.tableName()}", e)
}
}
import android.os.FileObserver
import android.util.Log
class DatabaseObserver<T: Any>(
private val db: SimpleDatabase<T>,
filePath: String = db.readableDatabase.path
): FileObserver(filePath, MODIFY) {
private val subscribers = HashSet<(SimpleDatabase<T>) -> Unit>()
override fun onEvent(event: Int, path: String?) {
subscribers.forEach {
try {
it(db)
} catch (e: Exception) {
Log.e(DatabaseObserver::class.java.simpleName, "Can not notify subscriber: $it")
}
}
}
fun subscribe(subscriber: (SimpleDatabase<T>) -> Unit) {
subscribers.add(subscriber)
}
fun unsubscribe(subscriber: (SimpleDatabase<T>) -> Unit) {
subscribers.remove(subscriber)
}
}
import android.content.Context
import android.util.Log
import kotlin.reflect.KClass
open class MutableListDatabase<M: Any> (
modelClass: KClass<M>,
context: Context,
databaseName: String,
version: Int,
defaultOrderBy: String? = null
) : SimpleDatabase<M>(modelClass, context, databaseName, version, defaultOrderBy), MutableList<M> {
override val size: Int
get() = size().toInt()
override fun contains(element: M): Boolean = find(element) != null
override fun containsAll(elements: Collection<M>): Boolean =
find(elements.getPrimaryFieldsSearchQuery()).size == elements.size
override fun get(index: Int): M =
findPage(index, 1, defaultOrderBy).firstOrNull() ?: throw IndexOutOfBoundsException("Index: $index, db size: $size")
override fun indexOf(element: M): Int {
forEachIndexed { index, m -> if (m == element) return index }
return -1
}
override fun isEmpty(): Boolean = size == 0
override fun iterator(): MutableIterator<M> = MutableDatabaseIterator(this)
override fun lastIndexOf(element: M): Int = indexOf(element)
override fun add(element: M): Boolean = insert(element)
override fun add(index: Int, element: M) {
writableDatabase.beginTransaction()
try {
val after = find(index, size - index, defaultOrderBy)
removeAll(after)
mutableListOf(element).plus(after).forEach {
insert(it)
}
writableDatabase.setTransactionSuccessful()
} catch (e: Exception) {
Log.e(MutableListDatabase::class.java.simpleName, e.message, e)
}
writableDatabase.endTransaction()
}
override fun addAll(index: Int, elements: Collection<M>): Boolean {
writableDatabase.beginTransaction()
try {
val after = find(index, size - index, defaultOrderBy)
removeAll(after)
elements.plus(after).forEach {
insert(it)
}
writableDatabase.setTransactionSuccessful()
} catch (e: Exception) {
Log.e(MutableListDatabase::class.java.simpleName, e.message, e)
writableDatabase.endTransaction()
return false
}
writableDatabase.endTransaction()
return true
}
override fun addAll(elements: Collection<M>): Boolean {
writableDatabase.beginTransaction()
try {
elements.forEach {
insert(it)
}
writableDatabase.setTransactionSuccessful()
} catch (e: Exception) {
Log.e(MutableListDatabase::class.java.simpleName, e.message, e)
writableDatabase.endTransaction()
return false
}
writableDatabase.endTransaction()
return true
}
override fun clear() {
remove()
}
override fun listIterator(): MutableListIterator<M> = MutableDatabaseListIterator(this)
override fun listIterator(index: Int): MutableListIterator<M> {
val listIterator = listIterator()
while (listIterator.hasNext() && listIterator.nextIndex() != index + 1) {
listIterator.next()
}
return listIterator
}
override fun removeAll(elements: Collection<M>): Boolean =
remove(elements.getPrimaryFieldsSearchQuery())
override fun removeAt(index: Int): M {
val toDelete = get(index)
remove(toDelete)
return toDelete
}
override fun retainAll(elements: Collection<M>): Boolean {
return removeAll(
this.filter {
!elements.contains(it)
}
)
}
override fun set(index: Int, element: M): M {
val old = get(index)
update(element, old.getPrimaryFieldsSearchQuery())
return old
}
override fun subList(fromIndex: Int, toIndex: Int): MutableList<M> =
find(orderBy = defaultOrderBy, limit = "$fromIndex,${toIndex - fromIndex}").toMutableList()
}
private open class MutableDatabaseIterator<T: Any>(
protected val dbList: MutableListDatabase<T>,
protected val pageSize: Int = 20
): MutableIterator<T> {
protected var currentPage = -1
protected var currentList = ArrayList<T>(pageSize)
protected var currentObject: T? = null
override fun hasNext(): Boolean {
return if (currentList.isNotEmpty()) {
true
} else {
currentPage++
refillList()
currentList.isNotEmpty()
}
}
override fun next(): T {
currentObject = currentList.removeAt(0)
return currentObject!!
}
override fun remove() {
currentObject ?. let {
dbList.remove(it)
}
}
protected fun refillList() {
currentList.clear()
currentList.addAll(dbList.findPage(currentPage, pageSize))
}
}
private class MutableDatabaseListIterator<T: Any>(
dbList: MutableListDatabase<T>,
pageSize: Int = 20
): MutableListIterator<T>, MutableDatabaseIterator<T>(dbList, pageSize) {
private val index: Int
get() = currentPage * pageSize + (pageSize - currentList.size)
private var previous: T? = null
override fun next(): T {
previous = currentObject
return super.next()
}
override fun hasPrevious(): Boolean = previous != null
override fun nextIndex(): Int = index + 1
override fun previous(): T = previous!!
override fun previousIndex(): Int = index - 1
override fun add(element: T) {
val currentSize = currentList.size
dbList.add(index, element)
refillList()
while (currentList.size > currentSize) {
currentList.removeAt(0)
}
}
override fun set(element: T) {
dbList[index] = element
currentObject = element
}
}
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.instanceParameter
import kotlin.reflect.full.memberProperties
@Target(AnnotationTarget.PROPERTY)
@MustBeDocumented
annotation class PrimaryKey
@Target(AnnotationTarget.PROPERTY)
@MustBeDocumented
annotation class Autoincrement
/**
* List of classes which can be primitive
*/
val nativeTypes = listOf(
Int::class,
Long::class,
Float::class,
Double::class,
String::class,
Boolean::class
)
/**
* @return Экземпляр KClass, содержащий данный KCallable объект.
*/
fun <T> KCallable<T>.intsanceKClass() : KClass<*> =
this.instanceParameter?.type?.classifier as KClass<*>
/**
* @return true если значение параметра может быть null.
*/
fun KCallable<*>.isNullable() : Boolean =
this.returnType.isMarkedNullable
/**
* @return Экземпляр KClass, возвращаемый KCallable.
*/
fun KCallable<*>.returnClass() : KClass<*> =
this.returnType.classifier as KClass<*>
/**
* @return true, если возвращает некоторый примитив.
*/
fun KCallable<*>.isReturnNative() : Boolean =
nativeTypes.contains(this.returnClass())
/**
* @return true если объект помечен аннотацией [PrimaryKey].
*/
fun KProperty<*>.isPrimaryField() : Boolean =
this.annotations.firstOrNull { it.annotationClass == PrimaryKey::class } != null
/**
* @return true если объект помечен аннотацией [Autoincrement].
*/
fun KProperty<*>.isAutoincrement() : Boolean {
this.annotations.forEach {
if (it.annotationClass == Autoincrement::class) {
return@isAutoincrement true
}
}
return false
}
/**
* @return Список полей класса.
*/
fun KClass<*>.getVariables() : List<KProperty<*>> =
this.memberProperties.toList()
import android.content.Context
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import kotlin.reflect.KClass
fun buildLimit(offset: Int? = null, limit: Int = 10): String {
return offset ?. let {
"$offset,$limit"
} ?: limit.toString()
}
open class SimpleDatabase<M: Any> (
protected val modelClass: KClass<M>,
context: Context,
databaseName: String,
version: Int,
protected val defaultOrderBy: String? = null
): SQLiteOpenHelper(context, databaseName, null, version) {
private var observer: DatabaseObserver<M>? = null
val databaseObserver: DatabaseObserver<M>
get() {
observer ?.let { return it }
observer = DatabaseObserver(this)
observer!!.startWatching()
return observer!!
}
override fun onCreate(db: SQLiteDatabase) {
db.createTableIfNotExist(modelClass)
}
override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) {
TODO("not implemented") //
// This will throw exception if you upgrade version of database but not
// override onUpgrade
}
open fun insert(value: M): Boolean {
return writableDatabase.insert(
modelClass.tableName(),
null,
value.toContentValues()
) > 0
}
open fun find(
where: String? = null,
orderBy: String? = defaultOrderBy,
limit: String? = null
): List<M> {
return readableDatabase.query(
modelClass.tableName(),
null,
where,
null,
null,
null,
orderBy,
limit
).extractAll(modelClass, true)
}
open fun find(
value: M
): M? = find(value.getPrimaryFieldsSearchQuery()).firstOrNull()
open fun findPage(
page: Int, size: Int, orderBy: String? = defaultOrderBy
): List<M> = find(page * size,size, orderBy)
open fun find(
offset: Int, size: Int, orderBy: String? = defaultOrderBy
): List<M> = find(orderBy = orderBy, limit = buildLimit(offset, size))
open fun update(
value: M,
where: String? = value.getPrimaryFieldsSearchQuery(),
onConflict: Int = SQLiteDatabase.CONFLICT_REPLACE
): Boolean {
return writableDatabase.updateWithOnConflict(
modelClass.tableName(),
value.toContentValues(),
where,
null,
onConflict
) > 0
}
open fun remove(where: String? = null): Boolean {
return writableDatabase.delete(
modelClass.tableName(),
where,
null
) > 0
}
open fun remove(element: M): Boolean {
return writableDatabase.delete(
modelClass.tableName(),
element.getPrimaryFieldsSearchQuery(),
null
) > 0
}
open fun size(where: String? = null): Long {
return DatabaseUtils.queryNumEntries(
readableDatabase,
modelClass.tableName(),
where,
null
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment