An ORM Wrapper for Native SQLite in Android SDKs
The article presents Sponsor, a lightweight ORM‑style wrapper for Android’s native SQLite that uses runtime annotations and dynamic proxies to generate Retrofit‑like, type‑safe CRUD APIs, automatically handling table creation, upgrades, and threading while keeping performance overhead negligible for SDK developers.
This article describes a practical ORM‑style wrapper built on top of the native SQLite API for Android. It targets SDK developers who cannot rely on third‑party ORM libraries (such as Room, GreenDao, or DBFlow) due to constraints on size, maintenance, or stability.
Implementation idea : Use dynamic proxies to intercept interface method calls, assemble SQL statements, and adapt the generic return type (raw type and actual type) to the appropriate execution path. The wrapper hides the raw SQL operations behind a clean, Retrofit‑like API.
Background
Although many mature ORM frameworks exist, they are unnecessary for ordinary apps and sometimes unsuitable for SDKs that must avoid external dependencies. Writing raw SQLite code manually is error‑prone (handling upgrades/downgrades, opening/closing, multithreading, SQL concatenation, ContentValues, cursor management, entity conversion, etc.). The proposed solution aims to simplify these repetitive tasks.
Expected outcomes
Reduce verbose CRUD code and eliminate fragile intermediate logic.
Automatically generate table‑creation and upgrade/downgrade scripts.
Provide an easy‑to‑use API that supports synchronous/asynchronous calls and thread switching.
Maintain stability and performance.
Design comparison
The author compares database operations with network requests, noting similar steps: request construction, execution, result handling, and error processing. This analogy leads to the decision to model database calls after Retrofit’s runtime‑annotation approach.
Annotation strategy
Compile‑time annotations (used by Room) were considered but deemed too cumbersome for SDK use. Instead, runtime annotations are employed, mirroring Retrofit’s method of using dynamic proxies to build Request objects and adapters to convert responses.
Key components
sponsor: core implementation.
sponsor_annotation: definition of runtime and compile‑time annotations.
sponsor_compiler: annotation processor that generates SQLiteOpenHelper subclasses for table creation and version management.
sponsor_java8 / sponsor_livedata / sponsor_rxjava2: adapters for different call‑style APIs.
Example code
1. Entity definition:
//Queryable: represents a queryable object with method bool convert(Cursor cursor)
//Insertable: represents an insertable object with method ContentValues convert()
public class FooEntity implements Queryable, Insertable {
/** database auto‑increment id */
private int id;
/** entity id */
private String fooId;
/** entity content */
private String data;
// other fields, getters/setters
}2. DAO interface (CRUD methods):
/** Insert */
@Insert(tableName = FooEntity.TABLE)
Call<Integer> insertEntities(List<FooEntity> entities);
/** Query */
@Query("SELECT * FROM " + FooEntity.TABLE + " WHERE " + FooEntity.CREATE_TIME + " > " + Parameter1.NAME + " AND " + FooEntity.CREATE_TIME + " < " + Parameter2.NAME + " ORDER BY " + FooEntity.CREATE_TIME + " ASC LIMIT " + Parameter3.NAME)
Call<List<FooEntity>> queryEntitiesByRange(@Parameter1 long start, @Parameter2 long end, @Parameter3 int limit);
/** Delete */
@Delete(tableName = FooEntity.TABLE, whereClause = FooEntity.ID + " >= " + Parameter1.NAME + " AND " + FooEntity.ID + " <= " + Parameter2.NAME)
Call<Integer> deleteByIdRange(@Parameter1 int startId, @Parameter2 int endId);3. Creating a Sponsor instance:
Sponsor sponsor = new Sponsor.Builder(this)
.allowMainThreadQueries() // optional main‑thread support
.logger(new SponsorLogger()) // logging
.build();
FooService fooService = sponsor.create(FooService.class);4. Inserting entities (sync & async):
// Synchronous
// int rowId = fooService.insertEntities(entities).execute();
// Asynchronous
fooService.insertEntities(entities).enqueue(new Callback<Integer>() {
@Override
public void onResponse(Call<Integer> call, Integer rowId) {
// success handling
}
@Override
public void onFailure(Call<Integer> call, Throwable t) {
// failure handling
}
});5. Querying and deleting:
List<FooEntity> result = fooService.queryEntitiesByRange(1, 200, 100).execute();
int deleted = fooService.deleteByIdRange(0, 100).execute();Core implementation snippets
Dynamic‑proxy entry point:
public <T> T create(final Class<T> daoClass, final Class<? extends DatabaseHelper> helperClass) {
Object obj = Proxy.newProxyInstance(daoClass.getClassLoader(), new Class<?>[]{daoClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
DaoMethod<Object, Object> daoMethod = (DaoMethod<Object, Object>) loadDaoMethod(method);
DatabaseHelper helper = loadDatabaseHelper(daoClass, helperClass);
Call<Object> call = new RealCall<>(helper, mDispatcher, mAllowMainThreadQueries, mLogger, daoMethod, args);
return daoMethod.adapt(call);
}
});
return (T) obj;
}Result‑converter selection:
private Converter<Response, ?> createQueryConverter(Type responseType, Class<?> rawType) {
if (Queryable.class.isAssignableFrom(rawType)) {
return new QueryableConverter((Class<? extends Queryable>) responseType);
} else if (rawType == List.class) {
return new ListQueryableConverter((Class<? extends Queryable>) argumentsTypes[0]);
} else if (rawType == Integer.class) {
return new IntegerConverter();
} else if (rawType == Long.class) {
return new LongConverter();
}
return null;
}RealCall execution (simplified):
public T execute() {
Response response = perform();
T value = null;
try {
value = mDaoMethod.toResponse(response);
} catch (Exception e) {
e.printStackTrace();
} finally {
// close cursor and database
if (response != null && response.getCursor() != null) {
try { response.getCursor().close(); } catch (Exception ignored) {}
}
if (mDatabaseHelper != null) {
try { mDatabaseHelper.close(); } catch (Exception ignored) {}
}
}
return value;
}Performance test
Test Item
Native (ms)
Sponsor (ms)
Remarks
Insert single record
44
45
Insert 100k records
8283
8214
Query one value among 100k
61
62
Query all 100k records
2747
2826
Reflection‑based entity creation adds ~79 ms
Update one value in 100k
43
44
Delete one record in 100k
22
22
Delete all 100k records
228
231
The sponsor implementation is slightly slower than raw SQLite calls because of the overhead of dynamic SQL assembly, but the difference is negligible for most use cases.
Generic type erasure discussion
Java generics are erased at runtime, but the compiler stores generic signatures in the class file’s Signature attribute. Retrofit (and the sponsor framework) retrieve this information via reflection on Method objects, allowing them to resolve return‑type arguments and convert JSON (or Cursor) data into the correct generic type.
Key points:
Class, method, field declarations retain generic signatures.
Local variable generic information is not recorded.
Interfaces such as ParameterizedType, TypeVariable, GenericArrayType, and WildcardType provide access to raw types, actual type arguments, bounds, etc.
Conclusion
The Sponsor library demonstrates that a Retrofit‑style, annotation‑driven approach can successfully abstract native SQLite operations, providing a concise, type‑safe, and extensible API for Android SDK development while keeping performance overhead minimal.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
