Mobile Development 18 min read

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.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
An ORM Wrapper for Native SQLite in Android SDKs

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceAndroidGenericsORMSQLiteDynamic Proxy
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.