Android数据库ORM框架用法、源码和性能比较分析

96
扬帆区块链
2016.03.09 15:36* 字数 1902

基本用法

LitePal

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,LitePal很“轻”,jar包只有100k不到,使用起来也比较简单,源码地址为Github地址

首先需要引入lib,可以通过gradle引入也可以将下载的litepal.jar包直接放入libs目录下。然后需要在assets目录下新建一个litepal.xml文件,文件名称不能随意更改,代码如下:

<?xml version="1.0" encoding="utf-8"?>  
<litepal>  
    <dbname value="demo" ></dbname>  
  
    <version value="1" ></version>  
  
    <list>  
        <mapping class="com.example.databasetest.model.News"></mapping>  
    </list>  
</litepal> 

<dbname>用于设定数据库的名字,<version>用于设定数据库的版本号,<list>用于设定所有的映射模型,<mapping>用于存放具体的Model类,必须是完整的类名。

然后还需要配置LitePalApplication,由于操作数据库时需要用到Context,而我们显然不希望在每个接口中都去传一遍这个参数,那样操作数据库就显得太繁琐了。因此,LitePal使用了一个方法来简化掉Context这个参数,只需要在AndroidManifest.xml中配置一下LitePalApplication,所有的数据库操作就都不用再传Context了。如果需要自定义application文件,只需继承LitePalApplication即可。

接下来需要创建Model类

public class News extends DataSupport {  
    private int id;       
    private String title;      
    private String content;        
    private Date publishDate;        
    private int commentCount;        
    // 自动生成get、set方法  
    ...  
}  

继承了DataSupport类之后,这些实体类就拥有了进行CRUD操作的能力

News news = new News();  
news.setTitle("这是一条新闻标题");  
news.setContent("这是一条新闻内容");  
news.setPublishDate(new Date());  
news.save(); 

List<News> newsList = DataSupport.select("title", "content")  
        .where("commentcount > ?", "0")  
        .order("publishdate desc").find(News.class);  

LitePal还提供了直接用sql原句实现查询的api方法,扩展性也比较好。

AFinal

AFinal是一个Android的orm、ioc快速开发框架,里面包含了四大功能:控件的id绑定和事件绑定功能;网络图片的显示功能(里面包含了强大的缓存框架);数据库sqlite的操作功能;http数据的读取功能(支持ajax方式读取),可从Github AFinal获取源码。数据库FinalDb是其中的一个组件,使用比较简单。

首先需要创建model类,承担对象与数据库表的映射功能。

@Table(name="table_user")
public class User {
    @id
    private int id;
    private String name;
    private String email;
    private Date birth;
    
    // getter and setter不能省略
    ...
}

声明@Table注解,表示表名称为table_user;@id表示id作为该表自动增长的主键,接下来就可以直接使用了。

FinalDb db = FinalDb.create(this, "afinal_db");
User user = new User();
user.setEmail("afinal@youzan.com");
user.setName("探索者");
user.setBirth(new Date());
db.save(user);
String name = "探索者";
List<User> userList = db.findAllByWhere(User.class, "name="' + name + "'" );//查询用户名为探索者的用户
if(userList.size() > 0){
    User other = userList.get(0);
    other.setEmail("customer@youzan.com");
    db.update(other);
}

以上就是AFinal中关于数据库的简单用法,不需要额外的配置,api接口也比较简单易用。

greenDAO

greenDAO与上述两种ORM框架不同,其原理不是根据反射进行数据库的各项操作,而是一开始就人工生成业务需要的Model和DAO文件,业务中可以直接调用相应的DAO文件进行数据库操作,从而避免了因反射带来的性能损耗和效率低下。但是由于需要人工生成model和DAO文件,所以greenDAO的配置就略显复杂。

首先需要新建一个java工程来生成DAO类文件,该工程需要导入greendao-generator.jar和freemarker.jar文件到项目中,github官方源码中已经提供了该工程。

public class ExampleDaoGenerator
{
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    public static void main(String[] args) throws Exception
    {
        Schema schema = new Schema(3, "de.greenrobot.daoexample");
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
        addNote(schema);
        addCustomerOrder(schema);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
        new DaoGenerator().generateAll(schema, "../DaoExample/src-gen");
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    private static void addNote(Schema schema)
    {
        Entity note = schema.addEntity("Note");
        note.addIdProperty();
        note.addStringProperty("text").notNull();
        note.addStringProperty("comment");
        note.addDateProperty("date");
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    private static void addCustomerOrder(Schema schema)
    {
        Entity customer = schema.addEntity("Customer");
        customer.addIdProperty();
        customer.addStringProperty("name").notNull();
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
        Entity order = schema.addEntity("Order");
        order.setTableName("ORDERS"); // "ORDER" is a reserved keyword
        order.addIdProperty();
        Property orderDate = order.addDateProperty("date").getProperty();
        Property customerId = order.addLongProperty("customerId").notNull().getProperty();
        order.addToOne(customer, customerId);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
        ToMany customerToOrders = customer.addToMany(order, customerId);
        customerToOrders.setName("orders");
        customerToOrders.orderAsc(orderDate);
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
}

在main方法中,

Schema schema = new Schema(3, "de.greenrobot.daoexample");

该方法第一个参数用来更新数据库版本号,第二个参数为要生成的DAO类所在包路径。然后进行建表和设置要生成DAO文件的目标工程的项目路径。

addNote(schema);
addCustomerOrder(schema);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
new DaoGenerator().generateAll(schema, "../DaoExample/src-gen");

其中src-gen这个目录名需要在运行前手动创建,否则会报错。运行后出现以下的提示说明DAO文件自动生成成功了,刷新一下DaoExample项目即可看到。

Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/NoteDao.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/Note.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/CustomerDao.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/Customer.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/OrderDao.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/Order.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/DaoMaster.java
Written /Users/duanyangyang/Documents/Android/greenDAO/DaoExample/src/main/java/de/greenrobot/daoexample/DaoSession.java
Processed 3 entities in 268ms

然后具体的使用就比较简单了

DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "notes-db", null);
db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
noteDao = daoSession.getNoteDao();

String textColumn = NoteDao.Properties.Text.columnName;
String orderBy = textColumn + " COLLATE LOCALIZED ASC";
cursor = db.query(noteDao.getTablename(), noteDao.getAllColumns(), null, null, null, null, orderBy);
String[] from = { textColumn, NoteDao.Properties.Comment.columnName };
int[] to = { android.R.id.text1, android.R.id.text2 };

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, cursor, from,
        to);
setListAdapter(adapter);

三种数据库框架性能比较

为了比较三种数据库框架的性能,首先选择同一款手机华为Mate7,然后从以下几个方面分析比较:

循环插入10000次,每次插入一条数据,三种框架用时为:

LitePal         155313ms;
AFinal          110601ms;
greedDAO        98238ms;

批量插入10000条数据,三种框架用时为:

LitePal         12882ms;
AFinal          2783ms;
greedDAO        623ms;

循环更新10000次,每次更新一条数据,三种框架用时为:

LitePal         152395ms;
AFinal          4123ms;
greedDAO        97840ms;

批量更新10000条数据,三种框架用时为:

LitePal         12234ms;
AFinal          1535ms;
greedDAO        739ms;

循环查询10000条数据,每次查询一条数据,三种框架用时为:

LitePal         13806ms;
AFinal          4117ms;
greedDAO        31923ms;

批量查询10000条数据,三种框架用时为:

LitePal         6863ms;
AFinal          1585ms;
greedDAO        149ms;

根据以上数据可以得出以下几个结论:

  • 不管是批量插入、更新、查询,greenDAO都是用时最短,执行速度最快的;
  • 大部分数据库操作的情况下,LitePal都是用时最长的,只有在循环查询时表现比greenDAO好
  • AFinal对于很多批量操作没有提供相应的api方法
  • 不管对于哪个框架,批量操作比循环操作用时都要短很多

下面是以greenDAO为例的测试源码

private void testInsertQueryTime(final int count){
    noteList1 = new ArrayList<Note>();
    for (int i = 0; i < count; i++){
        Note note = new Note(null, "title", "comment", new Date());
        noteList1.add(note);
    }

    noteList2 = new ArrayList<Note>();
    for (int i = 0; i < count; i++){
        Note note = new Note(null, "title", "comment", new Date());
        noteList2.add(note);
    }

    new Thread(){
        @Override
        public void run() {
            super.run();
            long time1 = System.currentTimeMillis();
            for(Note note : noteList1){
                noteDao.insert(note);
            }
            // 循环插入10000次,每次插入一条数据
            long time = System.currentTimeMillis() - time1;
            Log.d("NoteActivity", "greedDAO one by one insert " + noteList1.size() + " data, use time " + time + "ms");
       // 批量插入10000条数据
            long time2 = System.currentTimeMillis();
            noteDao.insertInTx((Note[])noteList2.toArray(new Note[noteList2.size()]));
            long time3 = System.currentTimeMillis() - time2;
            Log.d("NoteActivity", "greedDAO batch insert " + noteList2.size() + " data, use time " + time3 + "ms");

        // 循环查询10000条数据,每次查询一条数据
            long time4 = System.currentTimeMillis();
            for(int i = 0; i < count; i++){
                noteDao.queryBuilder().limit(1).list();
            }
            long time5 = System.currentTimeMillis() - time4;
            Log.d("NoteActivity", "greedDAO one by one query " + count + " data, use time " + time5 + "ms");

        // 批量查询10000条数据
            long time6 = System.currentTimeMillis();
            List<Note> list2 = noteDao.queryBuilder().limit(10000).offset(-1).list();
            long time7 = System.currentTimeMillis() - time6;
            Log.d("NoteActivity", "greedDAO batch query " + list2.size() + " data, use time " + time7 + "ms");

       // 循环更新10000次,每次更新一条数据
            long time8 = System.currentTimeMillis();
            for (Note note : noteList1){
                note.setText("update_title");
                note.setComment("update_comment");
                noteDao.update(note);
            }
            // 批量更新10000条数据
            long time9 = System.currentTimeMillis() - time8;
            Log.d("NoteActivity", "greedDAO one by one update " + count + " data, use time " + time9 + "ms");

            for (Note note : noteList2){
                note.setText("update_title");
                note.setComment("update_comment");
            }
            long time10 = System.currentTimeMillis();
            noteDao.updateInTx(noteList2);
            long time11 = System.currentTimeMillis() - time10;
            Log.d("NoteActivity", "greedDAO batch update " + count + " data, use time " + time11 + "ms");

        }
    }.start();

}

三种数据库框架的原理和源码分析

LitePal

LitePal通过LitePal.xml文件获取数据库的名称、版本号以及表,然后创建数据库和表,要执行增删改查操作时,就会根据数据model进行参数拼装,最后调用系统原生的数据库操作。解析litepal.xml文件方法源码如下:

private InputStream getConfigInputStream() throws IOException {
    AssetManager assetManager = LitePalApplication.getContext().getAssets();
    String[] fileNames = assetManager.list("");
    if (fileNames != null && fileNames.length > 0) {
        for (String fileName : fileNames) {
            if (Const.LitePal.CONFIGURATION_FILE_NAME.equalsIgnoreCase(fileName)) {
                return assetManager.open(fileName, AssetManager.ACCESS_BUFFER);
            }
        }
    }
    throw new ParseConfigurationFileException(
            ParseConfigurationFileException.CAN_NOT_FIND_LITEPAL_FILE);
}

void usePullParse() {
    try {
        LitePalAttr litePalAttr = LitePalAttr.getInstance();
        XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        XmlPullParser xmlPullParser = factory.newPullParser();
        xmlPullParser.setInput(getConfigInputStream(), "UTF-8");
        int eventType = xmlPullParser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {
            String nodeName = xmlPullParser.getName();
            switch (eventType) {
            case XmlPullParser.START_TAG: {
                if (NODE_DB_NAME.equals(nodeName)) {
                    String dbName = xmlPullParser.getAttributeValue("", ATTR_VALUE);
                    litePalAttr.setDbName(dbName);
                } else if (NODE_VERSION.equals(nodeName)) {
                    String version = xmlPullParser.getAttributeValue("", ATTR_VALUE);
                    litePalAttr.setVersion(Integer.parseInt(version));
                } else if (NODE_MAPPING.equals(nodeName)) {
                    String className = xmlPullParser.getAttributeValue("", ATTR_CLASS);
                    litePalAttr.addClassName(className);
                } else if (NODE_CASES.equals(nodeName)) {
                    String cases = xmlPullParser.getAttributeValue("", ATTR_VALUE);
                    litePalAttr.setCases(cases);
                }
                break;
            }
            default:
                break;
            }
            eventType = xmlPullParser.next();
        }
    } catch (XmlPullParserException e) {
        throw new ParseConfigurationFileException(
                ParseConfigurationFileException.FILE_FORMAT_IS_NOT_CORRECT);
    } catch (IOException e) {
        throw new ParseConfigurationFileException(ParseConfigurationFileException.IO_EXCEPTION);
    }
}

LitePal框架会根据model自动创建好相应的数据表,以及表数据类型和非空约束等。

protected String generateCreateTableSQL(String tableName, List<ColumnModel> columnModels,
            boolean autoIncrementId) {
    StringBuilder createTableSQL = new StringBuilder("create table ");
    createTableSQL.append(tableName).append(" (");
    if (autoIncrementId) {
        createTableSQL.append("id integer primary key autoincrement,");
    }
    if (columnModels.size() == 0) {
        createTableSQL.deleteCharAt(createTableSQL.length() - 1);
    }
    boolean needSeparator = false;
    for (ColumnModel columnModel : columnModels) {
        if (columnModel.isIdColumn()) {
            continue;
        }
        if (needSeparator) {
            createTableSQL.append(", ");
        }
        needSeparator = true;
        createTableSQL.append(columnModel.getColumnName()).append(" ").append(columnModel.getColumnType());
        if (!columnModel.isNullable()) {
            createTableSQL.append(" not null");
        }
        if (columnModel.isUnique()) {
            createTableSQL.append(" unique");
        }
        String defaultValue = columnModel.getDefaultValue();
        if (!TextUtils.isEmpty(defaultValue)) {
            createTableSQL.append(" default ").append(defaultValue);
        }
    }
    createTableSQL.append(")");
    LogUtil.d(TAG, "create table sql is >> " + createTableSQL);
    return createTableSQL.toString();
}

要执行增删改查操作的数据model都会继承DataSupport,以查询方法findAll()为例,DataSupport会先调用QueryHandler.onFindAll()方法,然后最后会执行到QueryHandler.query方法,该方法传入的参数有类名、列名称、查询条件、排序等等,经过处理后调用SDLiteDatabase.query方法,最后将查询得到的数据转换成List并返回,源码如下:

protected <T> List<T> query(Class<T> modelClass, String[] columns, String selection,
            String[] selectionArgs, String groupBy, String having, String orderBy, String limit,
            List<AssociationsInfo> foreignKeyAssociations) {
    List<T> dataList = new ArrayList<T>();
    Cursor cursor = null;
    try {
        List<Field> supportedFields = getSupportedFields(modelClass.getName());
        String tableName = getTableName(modelClass);
        String[] customizedColumns = getCustomizedColumns(columns, foreignKeyAssociations);
        cursor = mDatabase.query(tableName, customizedColumns, selection, selectionArgs,
                groupBy, having, orderBy, limit);
        if (cursor.moveToFirst()) {
            SparseArray<QueryInfoCache> queryInfoCacheSparseArray = new SparseArray<QueryInfoCache>();
            do {
                T modelInstance = (T) createInstanceFromClass(modelClass);
                giveBaseObjIdValue((DataSupport) modelInstance,
                        cursor.getLong(cursor.getColumnIndexOrThrow("id")));
                setValueToModel(modelInstance, supportedFields, foreignKeyAssociations, cursor, queryInfoCacheSparseArray);
                if (foreignKeyAssociations != null) {
                    setAssociatedModel((DataSupport) modelInstance);
                }
                dataList.add(modelInstance);
            } while (cursor.moveToNext());
            queryInfoCacheSparseArray.clear();
        }
        return dataList;
    } catch (Exception e) {
        e.printStackTrace();
        throw new DataSupportException(e.getMessage());
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

可以看到LitePal不管是创建数据库、表还是执行增删改查都是根据Model的类名和属性名,每次都需要进行反射拼装然后调用Android原生的数据库操作或者直接执行sql语句。

AFinal

AFinal本质上也是利用java的反射原理实现对象与数据表的映射的,实现上和LitePal有很多不同。AFinal并未提前创建数据库和表,而是在第一次执行增删改查方法时,先判断数据库和表是否存在,如果不存在的话就根据model执行sql语句进行创建,创建完成后再拼装具体的操作sql语句,完成相应的增删改查操作。相关源码如下:

public void save(Object entity) {
    checkTableExist(entity.getClass());
    exeSqlInfo(SqlBuilder.buildInsertSql(entity));
}

private void checkTableExist(Class<?> clazz) {
    if (!tableIsExist(TableInfo.get(clazz))) {
        String sql = SqlBuilder.getCreatTableSQL(clazz);
        debugSql(sql);
        db.execSQL(sql);
    }
}

public static String getCreatTableSQL(Class<?> clazz) {
    TableInfo table = TableInfo.get(clazz);

    Id id = table.getId();
    StringBuffer strSQL = new StringBuffer();
    strSQL.append("CREATE TABLE IF NOT EXISTS ");
    strSQL.append(table.getTableName());
    strSQL.append(" ( ");

    Class<?> primaryClazz = id.getDataType();
    if (primaryClazz == int.class || primaryClazz == Integer.class
            || primaryClazz == long.class || primaryClazz == Long.class) {
        strSQL.append(id.getColumn()).append(" INTEGER PRIMARY KEY AUTOINCREMENT,");
    } else {
        strSQL.append(id.getColumn()).append(" TEXT PRIMARY KEY,");
    }


    Collection<Property> propertys = table.propertyMap.values();
    for (Property property : propertys) {
        strSQL.append(property.getColumn());
        Class<?> dataType = property.getDataType();
        if (dataType == int.class || dataType == Integer.class
                || dataType == long.class || dataType == Long.class) {
            strSQL.append(" INTEGER");
        } else if (dataType == float.class || dataType == Float.class
                || dataType == double.class || dataType == Double.class) {
            strSQL.append(" REAL");
        } else if (dataType == boolean.class || dataType == Boolean.class) {
            strSQL.append(" NUMERIC");
        }
        strSQL.append(",");
    }

    Collection<ManyToOne> manyToOnes = table.manyToOneMap.values();
    for (ManyToOne manyToOne : manyToOnes) {
        strSQL.append(manyToOne.getColumn())
                .append(" INTEGER")
                .append(",");
    }
    strSQL.deleteCharAt(strSQL.length() - 1);
    strSQL.append(" )");
    return strSQL.toString();
}

greenDAO

greenDAO与上述两种ORM框架不同,其原理不是根据反射进行数据库的各项操作,而是一开始就人工生成业务需要的Model和DAO文件,业务中可以直接调用相应的DAO文件进行数据库的增删改查操作,从而避免了因反射带来的性能损耗和效率低下。以查询为例,其代码如下:

DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "notes-db", null);
db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
noteDao = daoSession.getNoteDao();
cursor = db.query(noteDao.getTablename(), noteDao.getAllColumns(), null, null, null, null, orderBy);

首先先创建数据库,然后在SQLiteOpenHelper.onCreate方法中根据已生成的model创建所有的表,而db.query其实就是Android原生的查询操作,只不过参数是经过DAO文件处理过的,无需手动匹配。其他数据库操作与查询雷同。

技术分享
Web note ad 1