"一般来说我们需要在Android操作数据库都是直接使用 SqliteOpenHelper 来实现。但这对不熟悉SQL语法的同学来说是比较痛苦的,而且Android的数据库接口乱七八糟的,更让人痛苦。为了让小伙伴们能愉快的使用数据库,很多人实现了ORM框架,比如GreenDAO, DBExecutor ...."

使用反射和注解实现 ORM 框架

一般来说我们需要在Android操作数据库都是直接使用 SqliteOpenHelper 来实现。但这对不熟悉SQL语法的同学来说是比较痛苦的,而且Android的数据库接口乱七八糟的,更让人痛苦。为了让小伙伴们能愉快的使用数据库,很多人实现了ORM框架,比如GreenDAO, DBExecutor, OrmLite等。为了实践一下之前所学习的反射与注解技术,因此决定自己从头写一个ORM的轮子。使用方式参考 JFinal 中的ActiveRecord思想。

一、接口定义

我们的目标是让用户只需要定义一个 JavaBean 继承于我们提供的 Model ,就可以实现增删改查的功能。
用户的使用步骤:

1. 声明数据库名和版本(可选)

用户需要在AndroidManifest.xml 的 application 标签中增加 meta-data 数据,指明数据库名,数据库版本信息

<meta-data android:name="dbHelper_dbName" android:value="user"/>
<meta-data android:name="dbHelper_dbVersion" android:value="1"/>

如果不指定dbName的话,默认值是应用的包名;如果不指定dbVersion的话,默认值是 1。

2. 定义 JavaBean 继承 Model 类

然后这个 JavaBean 类就拥有了以下方法

public class Model<M extends Model> {
    //获取数据库表名
    public String getTableName() {
        return null;
    }

    //获取用来专门用来查询的dao实例,这个实现只能调用查询方法,不能调用save, update, delete方法。
    public M dao() {
        return null;
    }

    //获取数据库表中的所有数据
    public List<M> findAll() {
        return null;
    }

    //根据sql语句来查询
    public List<M> find(String sql, Object... args) {
        return null;
    }

    //根据sql语句来查询,只返回第一个结果
    public M findFirst(String sql, Object... args) {
        return null;
    }

    //根据 id来查询
    public M findById(long id) {
        return null;
    }

    //保存本对象到数据库
    public boolean save() {
        return false;
    }

    //更新本对象到数据库
    public boolean update() {
        return false;
    }

    //从数据库中删除本对象
    public boolean delete() {
        return false;
    }

    //直接执行任意SQL语句
    public boolean exec(String sql, Object... args) {
        return false;
    }
}

3.注册所有 Model 类

我们提供一个类 DBManager 专门用来做数据库的管理,提供了 Model类的注册功能。
用户需要在 Application 的 onCreate 方法中注册所有的 Model 类。

DBManager.getInstance(this)
        .registerModel(User.class)
        .registerModel(Acrticle.class)
        .init();

然后用户就可以按下面的方法来使用数据库了:

//增加
User user = new User();
user.name = "qiushao";
user.addr = "china";
user.age = 28;
user.phone = "1234567890";
user.save();

//查询
List<User> userList = new User().dao().findAll();
for (User item : userList) {
    Log.d("qiushao", "user id = " + item.getId());
    Log.d("qiushao", "user name = " + item.name);
}

我们定义了上面的接口之后,就可以开始实现这些接口了。

二、解析数据库名,版本信息

在 DBManager.init 方法中提取数据库的版本信息:

public boolean init() {
    ApplicationInfo appInfo = null;
    try {
        appInfo = context.getPackageManager()
                .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }

    dbName = appInfo.metaData.getString("dbHelper_dbName");
    dbVersion = appInfo.metaData.getInt("dbHelper_dbVersion");
    if (dbName == null || dbName.trim().isEmpty()){
        dbName = context.getPackageName().replace(".", "_");
    }
    if (dbVersion <= 0) {
        dbVersion = 1;
    }
    Log.d(TAG, "dbName = " + dbName);
    Log.d(TAG, "dbVersion = " + dbVersion);
    dbo = new DBO(context, dbName, null, dbVersion, tableMap);
    return false;
}

三、使用反射技术解析 Model 类

在 DBManager.registerModel 的时候就对model进行解析,将解析的相关信息保存到一个 Table类中:

public DBManager registerModel(Class<? extends Model> clazz) {
    String tableName = clazz.getName().replace(".", "_");
    if (tableMap.containsKey(tableName)) {
        return this;
    }
    tableMap.put(clazz, parserModel(tableName, clazz));
    return this;
}

private Table parserModel(String tableName, Class<? extends Model> clazz) {
    Log.d(TAG, "tableName = " + tableName);
    Table table = new Table();
    List<Column> columns = new ArrayList<>();

    table.clazz = clazz;
    table.tableName = tableName;

    //获取 model 类的所有成员变量, 除了数据库不支持的类型,和使用 transient 关键字声明的
         Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        if (Modifier.isTransient(field.getModifiers()) || !DBType.isSupportType(field.getType())) {
            continue;
        }
        field.setAccessible(true);

        Column column = new Column(field, field.getName(), DBType.getDBType(field.getType()));
        columns.add(column);
        Log.d(TAG, "columnName = " + column.columnName);
    }

    table.columns = columns;
    return table;
}

这里用到了反射的以下方法:

  • 获取类名: String tableName = clazz.getName().replace(".", "_");
  • 获取成员变量信息: Field[] fields = clazz.getDeclaredFields();
  • 修改成员的访问权限: field.setAccessible(true);,这么设置之后,即使你是private变量,我也可以读写。
  • 获取成员变量名: field.getName()

四。创建数据库表

数据库信息,表结构信息我们都已经获取到了,下面就需要根据这些信息来创建数据库,和表结构了。
在此通过 SQLiteOpenHelper 来实现。新增一个类 DBO 继承 SQLiteOpenHelper :

class DBO extends SQLiteOpenHelper {
    private static final String TAG = "DBO";
    private Map<Class, Table> tableMap;

    public DBO(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, Map<Class, Table> tables) {
        super(context, name, factory, version);
        tableMap = tables;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        for (Map.Entry<Class, Table> entry : tableMap.entrySet()) {
            String tableName = entry.getValue().tableName;
            String sql = entry.getValue().getTableCreateSql();
            Log.d(TAG, "create table " + tableName);
            Log.d(TAG, "sql = " + sql);

            db.execSQL(sql);
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        for (Map.Entry<Class, Table> entry : tableMap.entrySet()) {
            String tableName = entry.getValue().tableName;
            String sql = entry.getValue().getTableDropSql();
            Log.d(TAG, "drop table " + tableName);
            Log.d(TAG, "sql = " + sql);

            db.execSQL(sql);
        }
        onCreate(db);
    }
}

这个类只负责数据库的创建和升级工作。
onCreate 和onUpgrade 方法是在调用 getWritableDatabase 或 getReadableDatabase 时,数据库未创建或者版本有升级时才会调用的。

五、实现增删改查接口

接下来我们只要实现 Model类的增删改查接口就行了。

  1. 增加 save 接口
    //保存本对象到数据库
    public boolean save() {
     if (false == modifyFlag) {
         throw new RuntimeException("dao instance can't call save method");
     }
     SQLiteDatabase db = DBManager.getInstance(null).getWritableDatabase();
     Table table = DBManager.getInstance(null).getTable(this.getClass());
     _id = db.insert(table.tableName, null, table.getContentValues(this));
     if (_id != -1) {
         return true;
     } else {
         return false;
     }
    }
    

这个函数的关键在于 getContentValues ,具体的实现也是通过反射技术实现的:

public ContentValues getContentValues(Model model) {
    ContentValues values = new ContentValues();
    try {
        for (Column column : columns) {
            switch (column.dbType) {
                case INTEGER:
                    values.put(column.columnName, column.field.getInt(model));
                    break;
                case LONG:
                    values.put(column.columnName, column.field.getLong(model));
                    break;
                case TEXT:
                    values.put(column.columnName, (String) column.field.get(model));
                    break;
            }
        }
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return values;
}

当然这里只实现了三种数据类型,需要扩展的话,再增加 case 分支即可。
这里用到的反射技术点:

  • 获取成员变量的值:column.field.getInt(model), column.field.getLong(model) 等。
  1. 实现查询接口:
    //获取数据库表中的所有数据
    public List<M> findAll() {
     List<M> list = new ArrayList<>();
     SQLiteDatabase db = DBManager.getInstance(null).getReadableDatabase();
     Table table = DBManager.getInstance(null).getTable(this.getClass());
     String sql  = table.getSelectAllSql();
     Cursor cursor = db.rawQuery(sql, null);
     while (cursor.moveToNext()) {
         M object  = table.cursorToInstance(cursor);
         object.setId(cursor.getLong(cursor.getColumnIndex("_id")));
         list.add(object);
     }
     cursor.close();
     return list;
    }
    

这里的关键是根据cursor构造一个model类出来 : cursorToInstance。

<M> M cursorToInstance(Cursor cursor) {
    Object object = null;
    try {
        object = clazz.newInstance();
        for (Column column : columns) {
            column.field.set(object, column.dbType.getValue(cursor, cursor.getColumnIndex(column.columnName)));
        }
    } catch (InstantiationException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
    return (M) object;
}

其他的接口实现就不再展开了,基本上都差不多。
使用到反射技术点:

  • 调用默认构造函数: object = clazz.newInstance();
  • 给成员变量赋值: column.field.set(object, column.dbType.getValue(cursor, cursor.getColumnIndex(column.columnName)));

至此,我们就完整的实现了一个最简单的 ORM 框架了。

六 . 使用注解实现数据库约束

上面实现的ORM框架是最最简单的,只是单纯的把一个个 javabean往数据库里面塞,没有做任何数据检查。
这对于真实使用场景是不够的,真实的使用场景是用户可能要求某个字段在表里面是唯一的,某个字段是不能为空的。
这就需要通过注解来实现了。我们通过 Unique 的注解来做示范。

  1. 增加 Unique 注解类

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Unique {
    }
    
  2. 在 Model类中使用 Unique注解:

    public class User extends Model<User> {
     @Unique
     public String name;
     public int age;
     public String addr;
     public String phone;
    }
    

3.修改Model解析,数据库表创建方法,读取 Unique注解
Model解析方法增加 field 是否被 Unique 注解判断。

private Table parserModel(String tableName, Class<? extends Model> clazz) {
   。。。
    for (Field field : fields) {
        if (Modifier.isTransient(field.getModifiers()) || !DBType.isSupportType(field.getType())) {
            continue;
        }
        field.setAccessible(true);

        Column column = new Column(field, field.getName(), DBType.getDBType(field.getType()));

        //判断这个 field 是否被 Unique 注解。
        if (null != field.getAnnotation(Unique.class)) {
            column.isUnique = true;
        }

    。。。
}

数据库表创建的时候增加 isUnique判断:

public String getTableCreateSql() {
    int index = 2;
    StringBuilder sql = new StringBuilder();
    sql.append("CREATE TABLE IF NOT EXISTS ");
    sql.append(tableName);
    sql.append("(_id integer primary key autoincrement,");

    for (Column column : columns) {
        sql.append(column.columnName);
        sql.append(" ");
        sql.append(column.dbType.getName());

        //是否 Unique
        if (column.isUnique) {
            sql.append(" unique ");
        }

        sql.append(",");
    }
    sql.deleteCharAt(sql.length() - 1);
    sql.append(")");

    return sql.toString();
}

这样子就完成了。我们增加数据库版本号,修改测试代码如下:

User user = new User();
user.name = "qiushao";
user.addr = "china";
user.age = 28;
user.phone = "1234567890";
user.save();

User user1 = new User();
user1.name = "qiushao";
user1.addr = "china";
user1.age = 28;
user1.phone = "1234567890";
user1.save();

运行后就会发现出错了:

E/SQLiteDatabase: Error inserting phone=1234567890 name=qiushao age=28 addr=china
                                                                          android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: net_qiushao_dbhelpertest_User.name (code 2067)
                                                                          #################################################################
                                                                          Error Code : 2067 (SQLITE_CONSTRAINT_UNIQUE)
                                                                          Caused By : Abort due to constraint violation.
                                                                              (UNIQUE constraint failed: net_qiushao_dbhelpertest_User.name (code 2067))
                                                                          #################################################################
                                                                              at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
                                                                              at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:866)
                                                                              at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
                                                                              at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
                                                                              at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1635)
                                                                              at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1505)
                                                                              at net.qiushao.dbhelper.Model.save(Model.java:106)
                                                                              at net.qiushao.dbhelpertest.MainActivity.onCreate(MainActivity.java:34)

其他的数据库约束都可以参考这个注解方式来进行实现,具体就不展开了。
最后,这个ORM框架只是用来说明反射和注解的用途而已,效率和健壮和稳定性,兼容性都是没有经过测试的,请勿使用于生产环境。
当然只需再完善一下,优化一下,应该就可以生产环境中使用的。
源码: https://gitee.com/qiushaox/DBHelper

0     0     0     0     0    
0 回帖