手写Android ORM库

ORM介绍

对象关系映射(Object Relational Mapping)简称ORM[1],用于实现面向对象编程语言里不同类型系统的数据之间的转换。简单的说,就是把数据库的表映射为类,列映射为类的属性,每一条数据映射为对象(类的实例)。

目标

重复造轮子,实现一个简单友好的Android SQLite ORM Library

  1. 支持根据类型自动建表与升级
  2. 支持执行自定义的初始化SQL脚本与升级SQL脚本
  3. 支持通过对方访问的方式进行表数据增删改查
  4. 支持注解方式配置表的属性与约束

分析、设计与实现

接口设计

SQL语法

先根据SQLite官方文档[2]定义用于自动建表与升级表的SQL语法

  • 自动建表语句格式
CREATE TABLE IF NOT EXIST <table_name> (
    ID INTEGER PRIMARY KEY AUTOINCREMENT,
    <text_column> TEXT [NOT] NULL [UNIQUE],
    <real_column> REAL [NOT] NULL [UNIQUE],
    <blob_column> BLOB [NOT] NULL,
    <int_column> INTEGER [NOT] NULL [UNIQUE],
    [UNIQUE (field1, field2) ON CONFLICT REPLACE]
)
  • 自动升级表结构
ALTER TABLE <table_name> ADD COLUMN <column_name> column_type [NOT] NULL

仅支持新增字段

自动建表

根据java模型创建数据库表,通过一系统映射规则生成建表语句后执行,同时也允许执行一段自定义的SQL脚本对数据库进行初始化。

  • 根据类型名或属性名生成表名或列名
  • 通过注解自定义表名或列名
  • 通过注解生成表约束
  • 自动根据java属性类型获取列类型

名字转换规则

类名和属性名中只允许包含字母、数字和_,建议采用驼峰命名法则,名字转换示例:

类或属性名 表名或列名
HelloWorld HELLO_WORLD
HElloWorld H_ELLO_WORLD
helloWorld HELLO_WORLD
helloWORld HELLO_WO_RLD
helloWorld0 HELLO_WORLD0

通过注解自定义名字

  • 类型注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface Table {

    /**
     * customized table name
     *
     * @return table name
     */
    String name() default "";

}
  • 属性注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface Column {

    /**
     * customized column name
     *
     * @return column name
     */
    String name() default "";

    /**
     * is column unique?
     *
     * @return unique or not
     */
    boolean unique() default false;

    /**
     * is column nullable?
     *
     * @return nullable or not?
     */
    boolean notNull() default false;

}

表约束

支持以下约束

  1. 必须包含id属性作为自增主键
  2. NULL or NOT NULL
  3. UNIQUE
  4. MULTI UNIQUE

类型映射

SQLite类型 Java类型
INTEGER Boolean,boolean,Short,short,Integer,int,Long,long,Date,Calendar
TEXT String,BigDecimal
REAL Double,double,Float,float
BLOB byte[]

生成建表语句

    static String createTableSQL(Class<?> table) {
        List<Field> columnFields = ReflectionUtils.getTableFields(table);
        String tableName = NamingUtils.toTableName(table);
        if (KeywordUtils.isKeyword(tableName)) {
            throw new InvalidNameException("Table name is keyword: " + tableName);
        }

        StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ");
        sb.append(tableName).append(" ( ID INTEGER PRIMARY KEY AUTOINCREMENT ");
        for (Field field : columnFields) {
            String columnName = NamingUtils.toColumnName(field);
            String columnType = TypeUtils.toColumnType(field.getType());

            if ("ID".equalsIgnoreCase(columnName)) {
                continue;
            }

            boolean notNull = false;
            boolean unique = false;
            if (field.isAnnotationPresent(Column.class)) {
                Column annotation = field.getAnnotation(Column.class);
                notNull = annotation.notNull();
                unique = annotation.unique();
            }
            if (field.isAnnotationPresent(NotNull.class)) {
                notNull = true;
            }
            if (field.isAnnotationPresent(Unique.class)) {
                unique = true;
            }

            sb.append(", ").append(columnName).append(" ").append(columnType);
            if (notNull) {
                sb.append(" NOT");
            }
            sb.append(" NULL");
            if (unique) {
                sb.append(" UNIQUE");
            }
        }

        if (table.isAnnotationPresent(MultiUnique.class)) {
            String[] constraint = table.getAnnotation(MultiUnique.class).value();
            if (constraint.length > 0) {
                sb.append(", UNIQUE(");
                for (String name : constraint) {
                    sb.append(NamingUtils.toSQLName(name)).append(",");
                }
                sb.delete(sb.length() - 1, sb.length());
                sb.append(") ON CONFLICT REPLACE");
            }
        }

        sb.append(" ) ");
        return sb.toString();
    }

执行自定义建表脚本

如果有自定义脚本,可以将脚本写在assets/scripts/create.sql文件中,当自动建表过程完成以后,执行此脚本完成自定义初始化过程。

升级数据库

自动升级表结构

通过对比java类型和已存在表结构,自动判断并将新增的列添加到数据库表中。

仅支持自动新增列,并且新增列不支持UNIQUE约束

    private static void addColumns(SQLiteDatabase db, Class<?> table) {
        List<Field> columnFields = ReflectionUtils.getTableFields(table);
        String tableName = NamingUtils.toTableName(table);
        List<String> existColumns = getColumnNames(db, tableName);
        List<String> alterCommands = new ArrayList<>();

        for (Field field : columnFields) {
            String columnName = NamingUtils.toColumnName(field);
            String columnType = TypeUtils.toColumnType(field.getType());

            if (existColumns.contains(columnName)) {
                continue;
            }

            boolean notNull = false;
            if (field.isAnnotationPresent(Column.class)) {
                Column annotation = field.getAnnotation(Column.class);
                notNull = annotation.notNull();
            }
            if (field.isAnnotationPresent(NotNull.class)) {
                notNull = true;
            }

            StringBuilder sb = new StringBuilder("ALTER TABLE ");
            sb.append(tableName).append(" ADD COLUMN ").append(columnName).append(" ").append(columnType);
            if (notNull) {
                sb.append(" NOT");
            }
            sb.append(" NULL");
            alterCommands.add(sb.toString());
        }

        for (String command : alterCommands) {
            db.execSQL(command);
        }
    }

执行自定义升级脚本

如果有自定义升级需求,可以将升级写在assets/scripts/<version>.sql文件中,version是数据库版本号。自定义脚本支持逐版本升级,比如数据库从版本1升级到版本10,在版本3,6,9有自定义升级脚本,那么assets/scripts目录下就存在3.sql,6.sql,9.sql这几个文件,数据库升级时会依次执行完成自定义升级。

增删改查

数据库创建好以后,通过调用系统提供的接口[3],进行增删改查这些最基本的数据访问操作。删除操作最简单,因为删除不涉及对象与表数据的映射,我们就先从删除开始。

删除数据

根据系统接口定义,再根据之前定义的主键约束,我们可以根据对象类型获取表名,根据id获取主键值,然后提供接系统接口就可以删除数据。我们提供2个删除接口:

  1. 根据主键删除
  2. 根据ORM对象删除。
public static boolean delete(Class<?> table, Long id);
public static boolean delete(Object o)

增加修改

增加和修改最大的特点就是要将ORM对象转为数据库表记录,是从对象到记录的映射。关于支持的数据类型与映射关系,请参考类型映射
增加和修改最终各自实现一个借口:

public static long save(Object o);
public static long update(Object o);

查询聚合

查询最大的特点就是要将数据库表记录转为ORM对象,是从记录到对象的映射。关于支持的数据类型与映射关系,请参考类型映射

查询可以有各种各样的查询方式,暂时提供几个接口:

public static <T> T fetch(Class<T> type, Long id);
public static <T> List<T> find(Class<T> type, String whereClause, String...args);

public static long count(Class<?> table);
public static long count(Class<?> table, String whereClause, String...whereArgs);

使用示例

暂无

性能优化

暂无

后续增强

通过前面实现的基本功能,已经可以满足大部分应用的需求了。当然还可以添加更多的功能以满足更多的需求:

  1. 实现类型自动扫描
  2. 支持事物管理
  3. 支持表之间的关系映射

以上只是随便说说,有兴趣可以自己动手实现

源码下载

https://github.com/hziee514/android-orm

参考资料


  1. ORM百科

  2. SQLite Query Language

  3. SQLiteDatabase

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 还在上学的时候,曾经一度最喜欢做的事情其实应该是写作,除开写作外,其他的大概只是算作学习一项技能。而写作对我来说并...
    DataMeat阅读 273评论 2 1
  • 文/素心说 茫茫大漠中,风自由地刮,沙自由地走,生活在这里的人却嗅不到太多自由。正在经历动荡的撒哈拉,无法主宰自己...
    素心说阅读 449评论 0 1
  • 昨天的昨天 如同一场挥霍的光阴 所以我说 一定 要留一些情意 给冬天的雪 在月色幽凉的夜晚 听风动寒枝的瑟瑟 听暖...
    白乐随心阅读 181评论 3 1