重复造轮子-自己写Android ORM库

作者: 核子飞弹 | 来源:发表于2018-06-09 12:27 被阅读10次

    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

    相关文章

      网友评论

        本文标题:重复造轮子-自己写Android ORM库

        本文链接:https://www.haomeiwen.com/subject/jlovsftx.html