移動端orm框架效能測評

williamwen1986發表於2019-07-15

移動端orm框架效能測評

flutter_orm_plugin 釋出以來,不少團隊試用了,我發現大家對這類資料庫相關的庫,第一反應就是效能如何,之前確實沒做太多行業對比,最近覺得還是有必要做一下效能測試,給大家一個交代的。

在ios端,業界比較常用的orm框架應該是蘋果官方推出的coredata,還有就是realm了。在android端orm框架我挑了三個比較常用的,greendao,realm和activeandroid。我會用flutter_orm_plugin跟上面提到的ios和android端orm框架做對比。 下面我會分別給出測試用例,測試程式碼,還有最終資料比較的結果。

測試用例

測試用例我列了以下這些

  • 10000次插入資料
  • 使用批量介面10000次插入資料
  • 10000次讀取資料
  • 10000次修改資料
  • 使用批量介面10000次修改資料
  • 10000次刪除資料
  • 使用批量介面10000次刪除資料

為什麼會有普通插入資料和使用批量介面插入資料的區別,大部分orm框架都會對批量操作有一定的優化,所以需要對批量操作進行測試,但是在平時使用,不一定都能用上批量介面(例如多次資料操作不在同一程式碼塊,或者在不同的模組中都要運算元據),所以我們會分別對普通操作和批量操作進行測試。

android 測試程式碼

首先我們給出flutter_orm_plugin 的測試程式碼,由於不想因為flutter和原生channel通訊產生誤差,我們直接用Luakit來寫lua程式碼做測試(greendao、realm、activeandroid、coredata都不涉及flutter和原生channel通訊),flutter_orm_plugin其實底層就是luakit的orm框架,這個不影響測試準確性。

迴圈插入

Luakit定義orm模型結構並做10000次插入,下面的程式碼是ios和android通用的。

	local Student = {
        __dbname__ = "test.db",
        __tablename__ = "Student",
        studentId = {"TextField",{primary_key = true}},
        name = {"TextField",{}},
        claName = {"TextField",{}},
        teacherName = {"TextField",{}},
        score = {"RealField",{}},
    }
     
    local params = {
        name = "Student",
        args = Student,
    }
    
    Table.addTableInfo(params,function ()
    	local studentTable = Table("Student”)
	    for i=1,10000 do
	           local s = {
	               studentId = "studentId"..i,
	               name = "name"..i,
	               claName = "claName"..i,
	               teacherName = "teacherName"..i,
	               score = 90,
	           }
	           studentTable(s):save()
	    end
 	end)

複製程式碼

activeandroid定義orm模型結構並做10000次插入


@Table(name = "Students")
public class Student extends Base {
    @Column(name = "studentId")
    public String studentId;
    @Column(name = "name")
    public String name;
    @Column(name = "claName")
    public String claName;
    @Column(name = "teacherName")
    public String teacherName;
    @Column(name = "score")
    public float score;
    public Student() {
        super();
    }
    @Override
    public String toString() {
        return this.studentId;
    }
}

for (int i=0 ; i<10000 ;i++) {
    ActiveAndroid.beginTransaction();
    Student s = new Student();
    s.studentId = "studentId"+i;
    s.name = "name"+i;
    s.teacherName = "teacherName"+i;
    s.claName = "claName"+i;
    s.score = 90;
    s.save();
    ActiveAndroid.setTransactionSuccessful();
    ActiveAndroid.endTransaction();
}

複製程式碼

realm android 定義orm模型結構並做10000次插入


public class StudentRealm extends RealmObject {
    @PrimaryKey
    private String studentId;
    @Required
    private String name;
    @Required
    private String teacherName;
    @Required
    private String claName;
    private float score;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getClaName() {
        return claName;
    }
    public void setClaName(String ClaName) {
        this.claName = claName;
    }
    public String getTeacherName() {
        return teacherName;
    }
    public void setTeacherName(String teacherName) {
        this.teacherName = teacherName;
    }
    public String getStudentId() {
        return studentId;
    }
    public void setStudentId(String id) {
        this.studentId = id;
    }
    public float getScore() {
        return score;
    }
    public void setScore(float score) {
        this.score = score;
    }
}
for (int i=0 ; i<10000 ;i++) {
    realm.beginTransaction();
    StudentRealm realmStudent = realm.createObject(StudentRealm.class,"studentId"+i);
    realmStudent.setName("name"+i);
    realmStudent.setTeacherName("setTeacherName"+i);
    realmStudent.setClaName("setClaName"+i);
    realmStudent.setScore(90);
    realm.commitTransaction();
}


複製程式碼

GreenDao定義orm模型結構並做10000次插入

@Entity()
public class Student {
    @Id
    private String studentId;
    @NotNull
    private String name;
    private String claName;
    private String teacherName;
    private float score;
    @Generated(hash = 1491230551)
    public Student(String studentId, @NotNull String name, String claName, String teacherName,
            float score) {
        this.studentId = studentId;
        this.name = name;
        this.claName = claName;
        this.teacherName = teacherName;
        this.score = score;
    }

    @Generated(hash = 1556870573)
    public Student() {
    }

    public String getStudentId() {
        return studentId;
    }
    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }
    @NotNull
    public String getName() {
        return name;
    }
    /** Not-null value; ensure this value is available before it is saved to the database. */
    public void setName(@NotNull String name) {
        this.name = name;
    }
    public String getClaName() {
        return claName;
    }
    public void setClaName(String claName) {
        this.claName = claName;
    }
    public String getTeacherName() {
        return teacherName;
    }
    public void setTeacherName(String teacherName) {
        this.teacherName = teacherName;
    }
    public float getScore() {
        return score;
    }
    public void setScore(float score) {
        this.score = score;
    }
}

DaoSession daoSession = ((App) getApplication()).getDaoSession();

StudentDao sd = daoSession.getStudentDao();
for (int i = 0; i < 10000; i++) {
    Student s = new Student();
    s.setStudentId("StudentId"+i);
    s.setClaName("getClaName"+i);
    s.setScore(90);
    s.setName("name"+i);
    s.setTeacherName("tn"+i);
    sd.insertOrReplace(s);
}

複製程式碼

批量插入

Luakit沒有提供批量插入介面。

active android批量插入10000條資料。


ActiveAndroid.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    Student s = new Student();
    s.studentId = "studentId"+i;
    s.name = "name"+i;
    s.teacherName = "teacherName"+i;
    s.claName = "claName"+i;
    s.score = 90;
    s.save();
}
ActiveAndroid.setTransactionSuccessful();
ActiveAndroid.endTransaction();


複製程式碼

realm android批量插入10000條資料。


Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    StudentRealm realmStudent = realm.createObject(StudentRealm.class,"studentId"+i);
    realmStudent.setName("name"+i);
    realmStudent.setTeacherName("setTeacherName"+i);
    realmStudent.setClaName("setClaName"+i);
    realmStudent.setScore(90);
}
realm.commitTransaction();

複製程式碼

GreenDao批量插入10000條資料


DaoSession daoSession = ((App) getApplication()).getDaoSession();
StudentDao sd = daoSession.getStudentDao();
ArrayList<Student> ss = new ArrayList<Student>();
for (int i = 0; i < 10000; i++) {
    Student s = new Student();
    s.setStudentId("StudentId"+i);
    s.setClaName("getClaName"+i);
    s.setScore(90);
    s.setName("name"+i);
    s.setTeacherName("tn"+i);
    ss.add(s);
}
sd.insertOrReplaceInTx(ss);

複製程式碼

###資料查詢

Luakit做10000次查詢,下面的程式碼是ios和android通用的。


local studentTable = Table("Student")
for i=1,10000 do
     local result = studentTable.get:where({"studentId"..i},"studentId = ?"):all()
end


複製程式碼

active android做10000次查詢。


for (int i=0 ; i<10000 ;i++) {
    List<Student> student = new Select()
            .from(Student.class)
            .where("studentId = ?", "studentId"+i)
            .execute();
}


複製程式碼

realm android 做10000次查詢。


for (int i=0 ; i<10000 ;i++) {
    RealmResults<StudentRealm> students = realm.where(StudentRealm.class).equalTo("studentId", "studentId"+i).findAll();
    List<StudentRealm> list = realm.copyFromRealm(students);
}

複製程式碼

GreenDao 做10000次查詢


DaoSession daoSession = ((App) getApplication()).getDaoSession();
StudentDao sd = daoSession.getStudentDao();
for (int i = 0; i < 10000; i++) {
    List<Student> s = sd.queryBuilder()
            .where(StudentDao.Properties.StudentId.eq("StudentId"+i))
            .list();
}

複製程式碼

###迴圈更新

Luakit做10000次更新。


local studentTable = Table("Student")
for i=1,10000 do
    local result = studentTable.get:where({"studentId"..i},"studentId = ?"):update({name = "name2”})
end


複製程式碼

active android做10000次更新。


for (int i=0 ; i<10000 ;i++) {
    ActiveAndroid.beginTransaction();
    Update update = new Update(Student.class);
    update.set("name = ?","name2")
            .where("studentId = ?", "studentId"+i)
            .execute();
    ActiveAndroid.setTransactionSuccessful();
    ActiveAndroid.endTransaction();
}


複製程式碼

realm android做10000次更新。


for (int i=0 ; i<10000 ;i++) {
    realm.beginTransaction();
    StudentRealm student = realm.where(StudentRealm.class).equalTo("studentId", "studentId"+i).findFirst();
    student.setClaName("ClaName"+(i+1));
    realm.copyToRealmOrUpdate(student);
    realm.commitTransaction();
}


複製程式碼

GreenDao做10000次更新。


for (int i = 0; i < 10000; i++) {
    List<Student> s = sd.queryBuilder()
            .where(StudentDao.Properties.StudentId.eq("StudentId"+i))
            .list();
    s.get(0).setName("name2");
    sd.update(s.get(0));
}

複製程式碼

###批量更新

Luakit沒有批量更新介面。

active android批量更新10000條資料。


ActiveAndroid.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    Update update = new Update(Student.class);
    update.set("name = ?","name2")
            .where("studentId = ?", "studentId"+i)
            .execute();
}
ActiveAndroid.setTransactionSuccessful();
ActiveAndroid.endTransaction();

複製程式碼

realm android批量更新10000條資料。


realm.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    StudentRealm student = realm.where(StudentRealm.class).equalTo("studentId", "studentId"+i).findFirst();
    student.setClaName("ClaName"+(i+1));
    realm.copyToRealmOrUpdate(student);
}
realm.commitTransaction();


複製程式碼

GreenDao批量更新10000條資料


ArrayList<Student> ss = new ArrayList<Student>();
for (int i = 0; i < 10000; i++) {
    List<Student> s = sd.queryBuilder()
            .where(StudentDao.Properties.StudentId.eq("StudentId"+i))
            .list();
    s.get(0).setName("name2");
    ss.add(s.get(0));
}
sd.updateInTx(ss);


複製程式碼

###迴圈刪除

Luakit做10000次刪除操作。


local studentTable = Table("Student")
for i=1,10000 do
     studentTable.get:where({"studentId"..i},"studentId = ?"):delete()
end

複製程式碼

active android做10000次刪除操作。


for (int i=0 ; i<10000 ;i++) {
    ActiveAndroid.beginTransaction();
    new Delete().from(Student.class).where("studentId = ?", "studentId"+i).execute();
    ActiveAndroid.setTransactionSuccessful();
    ActiveAndroid.endTransaction();
}

複製程式碼

realm android做10000次刪除操作。


for (int i=0 ; i<10000 ;i++) {
    realm.beginTransaction();
    StudentRealm student = realm.where(StudentRealm.class).equalTo("studentId", "studentId"+i).findFirst();
    student.deleteFromRealm();
    realm.commitTransaction();
}


複製程式碼

GreenDao做10000次刪除操作。


for (int i = 0; i < 10000; i++) {
    List<Student> s = sd.queryBuilder()
            .where(StudentDao.Properties.StudentId.eq("StudentId"+i))
            .list();
    s.get(0).setName("name2");
    sd.delete(s.get(0));
}

複製程式碼

###批量刪除

Luakit沒有批量刪除介面。

active android批量刪除10000條資料。


ActiveAndroid.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    new Delete().from(Student.class).where("studentId = ?", "studentId"+i).execute();
}
ActiveAndroid.setTransactionSuccessful();
ActiveAndroid.endTransaction();


複製程式碼

realm android批量刪除10000條資料。


realm.beginTransaction();
for (int i=0 ; i<10000 ;i++) {
    StudentRealm student = realm.where(StudentRealm.class).equalTo("studentId", "studentId"+i).findFirst();
    student.deleteFromRealm();
}
realm.commitTransaction();

複製程式碼

GreenDao批量刪除10000條資料。


ArrayList<Student> ss = new ArrayList<Student>();
for (int i = 0; i < 10000; i++) {
    List<Student> s = sd.queryBuilder()
            .where(StudentDao.Properties.StudentId.eq("StudentId"+i))
            .list();
    ss.add(s.get(0));
}
sd.deleteInTx(ss);


複製程式碼

android 測試結果及分析

下面給出測試結果,表格中所有資料的單位是秒,即做10000次操作需要的秒數。

image

image

image

  • 可以看到,active android各項效能都一般。

  • 在使用批量介面的情況下GreenDao和Realm的效能比較好。

  • 在使用批量介面的情況下Realm的效能尤其好,批量插入、查詢、批量更改、批量刪除都是Realm的效能最好,但是Realm的非批量介面效能較差,所有可以這樣總結,如果程式碼高內聚,可以把資料操作程式碼入口都統一使用,Realm效能是最好的,但這對程式碼質量、模組設計有要求,當運算元據的程式碼到處都有,不能使用批量介面時,Realm的效能是不好的。

  • Luakit沒有提供批量介面,但從圖中可以看出,Luakit的各項效能指標都是比較好的,而且對程式碼沒有要求,即使運算元據的程式碼不內聚,也不會對效能有影響。

ios測試程式碼

Luakit是跨平臺的,程式碼跟android一樣,下面就不列了,只給出Coredata和 Realm ios

迴圈插入

Coredata 定義orm模型結構並做10000次插入


@interface Student (CoreDataProperties)

+ (NSFetchRequest<Student *> *)fetchRequest;

@property (nullable, nonatomic, copy) NSString *claName;
@property (nullable, nonatomic, copy) NSString *name;
@property (nonatomic) float score;
@property (nullable, nonatomic, copy) NSString *studentId;
@property (nullable, nonatomic, copy) NSString *teacherName;

@end

self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
for (int i=0; i<10000; i++) {
    Student *s = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.context];
    s.studentId = [NSString stringWithFormat:@"studentId%d",i];
    s.name = [NSString stringWithFormat:@"name%d",i];
    s.teacherName = [NSString stringWithFormat:@"teacherName%d",i];
    s.claName = [NSString stringWithFormat:@"claName%d",i];
    s.score = 90;
    NSError *error = nil;
    [self.context save:&error];
}


複製程式碼

Realm ios定義orm模型結構並做10000次插入


@interface StudentRLM : RLMObject

@property NSString *studentId;
@property NSString *name;
@property NSString *teacherName;
@property NSString *claName;
@property float score;

@end



for (int i=0; i<10000; i++) {
    [realm beginWriteTransaction];
    StudentRLM *s = [[StudentRLM alloc] init];
    s.studentId = [NSString stringWithFormat:@"studentId%d",i];;
    s.name = [NSString stringWithFormat:@"name%d",i];
    s.teacherName = [NSString stringWithFormat:@"teacherName%d",i];
    s.claName = [NSString stringWithFormat:@"claName%d",i];
    s.score = 90;
    [realm addOrUpdateObject:s];
    [realm commitWriteTransaction];
    [realm beginWriteTransaction];
}

複製程式碼

批量插入

Coredata 批量插入10000條資料。


for (int i=0; i<10000; i++) {
    Student *s = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.context];
    s.studentId = [NSString stringWithFormat:@"studentId%d",i];
    s.name = [NSString stringWithFormat:@"name%d",i];
    s.teacherName = [NSString stringWithFormat:@"teacherName%d",i];
    s.claName = [NSString stringWithFormat:@"claName%d",i];
    s.score = 90;
    
}
NSError *error = nil;
[self.context save:&error];


複製程式碼

Realm ios批量插入10000條資料。


[realm beginWriteTransaction];
for (int i=0; i<10000; i++) {
    StudentRLM *s = [[StudentRLM alloc] init];
    s.studentId = [NSString stringWithFormat:@"studentId%d",i];;
    s.name = [NSString stringWithFormat:@"name%d",i];
    s.teacherName = [NSString stringWithFormat:@"teacherName%d",i];
    s.claName = [NSString stringWithFormat:@"claName%d",i];
    s.score = 90;
    [realm addOrUpdateObject:s];
}
[realm commitWriteTransaction];
[realm beginWriteTransaction];


複製程式碼

查詢

Coredata 做10000次查詢。


for (int i=0; i<10000; i++) {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    request.predicate = [NSPredicate predicateWithFormat:@"studentId = 'studentId%d'"];
    NSArray *objs = [self.context executeFetchRequest:request error:&error];
}

複製程式碼

Realm ios做10000次查詢。


for (int i=0; i<10000; i++) {
    RLMResults *results  = [StudentRLM objectsWhere: [NSString stringWithFormat:@"studentId = 'studentId%d'",i]];
    StudentRLM *s = results.firstObject;
}

複製程式碼

###迴圈更新

Coredata 做10000次更新。


for (int i=0; i<10000; i++) {
    NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntityName:@"Student"];
    batchUpdateRequest.predicate = [NSPredicate predicateWithFormat:@"studentId = 'studentId%d'"];
    batchUpdateRequest.propertiesToUpdate = @{@"name" : @"name2"};
    batchUpdateRequest.resultType = NSUpdatedObjectsCountResultType;
    NSBatchUpdateResult *batchResult = [self.context executeRequest:batchUpdateRequest error:&error];
    NSError *error = nil;
    [self.context save:&error];
}

複製程式碼

Realm ios做10000次更新。


for (int i=0; i<10000; i++) {
    [realm beginWriteTransaction];
    RLMResults *results  = [StudentRLM objectsWhere: [NSString stringWithFormat:@"studentId = 'studentId%d'",i]];
    NSLog(@"results %lu",(unsigned long)[results count]);
    StudentRLM *s = results.firstObject;
    [s setName:@"name"];
    [realm addOrUpdateObject:s];
    [realm commitWriteTransaction];
}

複製程式碼

###批量更新

Coredata 批量更新10000條資料。


for (int i=0; i<10000; i++) {
    NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntityName:@"Student"];
    batchUpdateRequest.predicate = [NSPredicate predicateWithFormat:@"studentId = 'studentId%d'"];
    batchUpdateRequest.propertiesToUpdate = @{@"name" : @"name2"};
    batchUpdateRequest.resultType = NSUpdatedObjectsCountResultType;
    NSBatchUpdateResult *batchResult = [self.context executeRequest:batchUpdateRequest error:&error];
}
NSError *error = nil;
[self.context save:&error];


複製程式碼

Realm ios批量更新10000條資料。


[realm beginWriteTransaction];
for (int i=0; i<10000; i++) {
    RLMResults *results  = [StudentRLM objectsWhere: [NSString stringWithFormat:@"studentId = 'studentId%d'",i]];
    NSLog(@"results %lu",(unsigned long)[results count]);
    StudentRLM *s = results.firstObject;
    [s setName:@"name”];
    [realm addOrUpdateObject:s];
}
[realm commitWriteTransaction];

複製程式碼

###迴圈刪除

Coredata 做10000次刪除操作。


for (int i=0; i<10000; i++) {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    request.predicate = [NSPredicate predicateWithFormat:@"studentId = 'studentId%d'"];
    NSBatchDeleteRequest *batchRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
    batchRequest.resultType = NSUpdatedObjectsCountResultType;
    NSBatchUpdateResult *batchResult = [self.context executeRequest:batchRequest error:&error];
    NSError *error = nil;
    [self.context save:&error];
}

複製程式碼

Realm ios做10000次刪除操作。


for (int i=0; i<10000; i++) {
    [realm beginWriteTransaction];
    RLMResults *results  = [StudentRLM objectsWhere: [NSString stringWithFormat:@"studentId = 'studentId%d'",i]];
    StudentRLM *s = results.firstObject;
    [s setName:@"name"];
    [realm deleteObject:s];
    [realm commitWriteTransaction];
}

複製程式碼

###批量刪除

Coredata 批量刪除10000條資料。


for (int i=0; i<10000; i++) {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    request.predicate = [NSPredicate predicateWithFormat:@"studentId = 'studentId%d'"];
    NSBatchDeleteRequest *batchRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
    batchRequest.resultType = NSUpdatedObjectsCountResultType;
    NSBatchUpdateResult *batchResult = [self.context executeRequest:batchRequest error:&error];
}
NSError *error = nil;
[self.context save:&error];


複製程式碼

Realm ios批量刪除10000條資料。


[realm beginWriteTransaction];
for (int i=0; i<10000; i++) {
    RLMResults *results  = [StudentRLM objectsWhere: [NSString stringWithFormat:@"studentId = 'studentId%d'",i]];
    StudentRLM *s = results.firstObject;
    [s setName:@"name"];
    [realm deleteObject:s];
}
[realm commitWriteTransaction];

複製程式碼

ios 測試結果及分析

下面給出測試結果,表格中所有資料的單位是秒,即做10000次操作需要的秒數。

image

image

image

  • 可以看到,Coredata除了批量插入效能是最好的以外,其他項效能都一般。

  • Realm ios和Realm android效能非常相似,批量操作效能優異,但是非批量操作效能一般。可以這樣總結,如果程式碼高內聚,可以把資料操作程式碼入口都統一使用,Realm效能是最好的,但這對程式碼質量、模組設計有要求,當運算元據的程式碼到處都有,不能使用批量介面時,Realm的效能是不好的。

  • Luakit沒有提供批量介面,但從圖中可以看出,Luakit的各項效能指標都是比較好的,而且對程式碼沒有要求,即使運算元據的程式碼不內聚,也不會對效能有影響。

相關文章