写一个简单的Android版本的ORM

几年前刚刚涉及做Android开发时候毕竟头大的是数据库相关的增删改查,因为项目要对离线数据进行管理数据库操作比较多,不断通过查询Cursor拼装成对象,完了还要close cursor,重复工作太多太累,曾经用过Hibernate只是Android平台没有罢了,那会儿也还没有Android平台的权威级的ORM。
经过项目实践有意或无意做了一个如下操作DB的小lib,简化了不少重复劳动.

想法起源:

最初的萌芽来自于Android Email App内部一个叫EmailContent的class定义。每个Table类都手动实现toContentValues()和restore(Cursor cursor),这样可以避免重复通过Cursor拼装表对象。随后自己就想写了一个Utils类似的工具类类来提供一系列增删改查的API,当然这些API的操作对象都是table对象,随着迭代慢慢衍化如今更加友好的light-dao了。

下面描述下如何使用此light-dao:

1. 得定义一个继承BaseDBHelper的DBHelper,大家都懂的:

public class DBHelper extends BaseDBHelper {
    private static final String DATABASE_NAME = "school.db";
    private static final int VERSION = 1;

    @SuppressLint("StaticFieldLeak")
    private static DBHelper sSingleton;

    private DBHelper(Context context) {
        super(context, DATABASE_NAME, VERSION);
    }

    private static DBHelper getSingleton(Context context) {
        if (sSingleton == null) {
            synchronized (DBHelper.class) {
                sSingleton = new DBHelper(context.getApplicationContext());
            }
        }
        return sSingleton;
    }

    public static DBUtils with(Context context) {
        return DBUtils.create(getSingleton(context));
    }

    /**
     * all table classes should configured here
     *
     * @param tableClasses table classes
     */
    @Override
    protected void onClassLoad(List<Class<? extends Entity>> tableClasses) {
        tableClasses.add(Student.class);
        tableClasses.add(Teacher.class);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        super.onUpgrade(db, oldVersion, newVersion);

        /* upgrade db version by version
        if (oldVersion < VERSION) {
            SQL sql = SQLBuilder.buildTableCreateSQL(Student.class);
            db.execSQL(sql.getSql());
        }
        */
    }
}

2. 然后,定义你的各种表的类,如下为学生表和老师表,且学生表里有老师表ID的外键:

@Table("teacher")
public class Teacher extends Entity {
    @Column(name = "name", notnull = true)
    public String name;
}  

@Table("student")
public class Student extends Entity {
    @Foreign(Teacher.class)
    @Column(name = "teacher_id", notnull = true)
    public long teacherId;

    @Column(name = "name", notnull = true)
    public String name;

    @Column(name = "age", notnull = true)
    public Integer age;
}

3. 最后就可以通过lightdao进行常见的数据库增删改查了:

3.1 单个保存

Teacher teacher = new Teacher();
teacher.name = "王老师";
long teacherId = DBHelper.with(mContext).save(teacher);

// 老师和学生关系是一对多,因此学生表中有老师表的ID作为外键
Student student = new Student();
student.teacherId = teacherId;
student.name = "小学生";
student.age = 20;
long id = DBHelper.with(mContext).save(student);
assertTrue(id > 0);

3.2 批量保存

List<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Student student = new Student();
    student.name = "name " + i;
    student.age = i;
    student.teacherId = 1; // 假设都是ID为1的教师的学生
    students.add(student);
}
int count = DBHelper.with(mContext).saveAll(students);
assertTrue(count == students.size());

3.3 数量查询

int count = DBHelper.with(mContext)
    .withTable(Student.class)
    .withWhere("age > ?", 5)
    .applyCount();
assertTrue(count > 0);

3.5 根据主键ID查找

Student student = DBHelper.with(mContext)
    .withTable(Student.class)
    .applySearchById(1);
assertTrue(student != null);

3.6 查询所有并以list返回结果

List<Student> students = DBHelper.with(mContext)
    .withTable(Student.class)
    .applySearchAsList();
assertTrue(students.size() > 0);

3.7 带有条件查询并以list返回结果

// 类似的还有很多其他以“with”开头的API,如:
// withColumns: 只查询指定的column
// withGroupBy: 查询分组
// withHaving: 分组后的条件筛选
// withOrderBy: 排序控制
// withLimit: 分页控制
// withDistinct: 查询去重

List<Student> students = DBHelper.with(mContext)
    .withTable(Student.class)
    .withWhere("age>?", 5)
    .applySearchAsList();
assertTrue(students.size() > 0);

3.8 更新部分字段

ContentValues values = new ContentValues();
values.put("name", "hello baby");

int count = DBHelper.with(mContext)
        .withTable(Student.class)
        .withWhere("age<?", 5)
        .applyUpdate(values);
assertTrue(count > 0);

3.9 根据对象更新

DBUtils dbUtils = DBHelper.with(mContext);
Student student = dbUtils.withTable(Student.class).applySearchById(1);
assertTrue(student != null);

student.name = "testUpdateTable";
int count = dbUtils.withTable(Student.class).applyUpdate(student);
assertTrue(count > 0);

4.0 根据主键ID删除

int count = DBHelper.with(mContext).withTable(Student.class).applyDeleteById(1);
assertTrue(count > 0);

4.1 删除指定的对象

DBUtils dbUtils = DBHelper.with(mContext);
Student student = dbUtils.withTable(Student.class).applySearchById(2);
assertTrue(student != null);

int count = dbUtils.withTable(Student.class).applyDelete(student);
assertTrue(count > 0);

4.2 根据条件删除

int count = DBHelper.with(mContext).withTable(Student.class).withWhere("age>=?", 9).applyDelete();
assertTrue(count > 0);

4.3 批处理(数据库事务)

BatchJobs jobs = new BatchJobs();
Student student = new Student();
student.name = "insert from batch job";
student.age = 1;
jobs.addInsertJob(student);

// update with table object
student = DBHelper.with(mContext).withTable(Student.class).applySearchFirst();
student.name = "updated from batch job";
jobs.addUpdateJob(Student.class, student);

// update with id
jobs.addUpdateJob(Student.class, student.id, student.toContentValues());

// update with condition
jobs.addUpdateJob(Student.class, student.toContentValues(), "age=?", 6);

// delete with table object
jobs.addDeleteJob(student);

// delete with id
jobs.addDeleteJob(Student.class, 7);

// delete with condition
jobs.addDeleteJob(Student.class, "age<?", 3);

boolean success = DBHelper.with(mContext).applyBatchJobs(jobs);
assertTrue(success);

4.4 跨表查询

// 因为跨表查询的结果来自于多个表,所以得重新定义返回结果的对象,并通过aliasName指定此字段来自于哪个表中的哪个字段
public class Relation extends Query {
    @Column(name = "teacher_id", aliasName = "student._id as teacher_id")
    public long teacherId;
    
    @Column(name = "teacher_name", aliasName = "teacher.name as teacher_name")
    public String teacherName;
    
    @Column(name = "student_id", aliasName = "student._id as student_id")
    public long studentId;
    
    @Column(name = "student_name", aliasName = "student.name as student_name")
    public String studentName;
    
    @Column(name = "age")
    public int studentAge;
}

List<Relation> list = DBHelper.with(mContext)
        .withQuery(Relation.class)
        .applySearchAsList();
System.out.println(list.size());

4.5 数据库升级

数据库升级其实啥也不用做,因为是自动的,因为重写了onUpgrade():

@Override
public final void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    autoMigrate(db, mTableClasses);
}

private void autoMigrate(SQLiteDatabase db, List<Class<? extends Entity>> tableClasses) {
    for (Class<? extends Entity> clazz : tableClasses) {
        String tableName = ReflectTools.getTableName(clazz);
        boolean exist = ReflectTools.isTableExist(db, tableName);
        if (exist) {
            Field[] fields = ReflectTools.getClassFields(clazz);
            for (Field field : fields) {
                Column column = field.getAnnotation(Column.class);
                if (column == null) {
                    continue;
                }

                String columnName = !TextUtils.isEmpty(column.name()) ? column.name() : field.getName();
                String dataType = ReflectTools.getDataTypeByField(field);
                boolean columnExist = ReflectTools.isColumnExist(db, tableName, columnName);
                if (!columnExist) {
                    db.execSQL("ALTER TABLE " + tableName + " ADD " + columnName + " " + dataType);
                }
            }
        } else {
            db.execSQL(SQLBuilder.buildCreateSQL(clazz).getSql());
        }
    }
}

static boolean isTableExist(SQLiteDatabase db, String tableName) {
    Cursor cursor = null;
    try {
        cursor = db.rawQuery("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", new String[]{tableName});
        boolean hasNext = cursor.moveToNext();
        return hasNext && cursor.getInt(0) > 0;
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

static boolean isColumnExist(SQLiteDatabase db, String tableName, String columnName) {
    Cursor cursor = null;
    try {
        cursor = db.rawQuery("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE ? OR sql LIKE ?);",
                new String[]{tableName, "%(" + columnName + "%", "%, " + columnName + " %"});
        boolean hasNext = cursor.moveToNext();
        return hasNext && cursor.getInt(0) > 0;
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

相关实现可查阅light-dao:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容