ORM介绍
对象关系映射(Object Relational Mapping)简称ORM[1],用于实现面向对象编程语言里不同类型系统的数据之间的转换。简单的说,就是把数据库的表映射为类,列映射为类的属性,每一条数据映射为对象(类的实例)。
目标
重复造轮子,实现一个简单友好的Android SQLite ORM Library
- 支持根据类型自动建表与升级
- 支持执行自定义的初始化SQL脚本与升级SQL脚本
- 支持通过对方访问的方式进行表数据增删改查
- 支持注解方式配置表的属性与约束
分析、设计与实现
接口设计
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;
}
表约束
支持以下约束
- 必须包含id属性作为自增主键
- NULL or NOT NULL
- UNIQUE
- 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个删除接口:
- 根据主键删除
- 根据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);
使用示例
暂无
性能优化
暂无
后续增强
通过前面实现的基本功能,已经可以满足大部分应用的需求了。当然还可以添加更多的功能以满足更多的需求:
- 实现类型自动扫描
- 支持事物管理
- 支持表之间的关系映射
以上只是随便说说,有兴趣可以自己动手实现
源码下载
https://github.com/hziee514/android-orm