Last active
November 25, 2017 10:04
-
-
Save InsanusMokrassar/36938477424d8503df94eac06c005c9d to your computer and use it in GitHub Desktop.
Very simple ORM CRUD Android SQL managment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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