Overview
ORM or Object Relational Mapping is a technique that allows you to query and manipulate data on a database using an object-oriented model. Thereby allowing you to directly manipulate data on the database through objects, not SQL statements.
Here is an example when not using ORM:
String sql = "INSERT INTO t_person VALUES (username=\"buithanh\", password=\"password\")";
database.exeSQL(sql);
When using ORM
Person p = new Person("buithanh", "password");
p.save();
Pros
Cons
Some ORM libraries
Building ORM library
In this article, I only introduce how to build an ORM library on Java language and Android platform only.
As we all know, any relational database is composed of tables and tables include records that store information about an entity - an entity. As for OOP, everything is an object, and an object is also a unit of information storage like a record in a relational database.
Thus we see that there is a correspondence that class name is equivalent to table name, an object is equivalent to a record and its attributes are equivalent to fields. And here are the steps to build the library.
Create a new annotation type
Annotation: A form of metadata that provides information about the program and it has no direct influence on the code it annotates. Objects that can use annotations are: classes, methods, fields, parameters and packages.
So somehow through annotation we can know which class corresponds to which table? Which field does that attribute correspond to? and what properties do the fields have? such as "PRIMARY KEY", "NOT NULL", "UNSIGNED"...
Create annotation for Class
@Documented
@Target(ElementType.TYPE)
@Inherited
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Table {
public static final String ID_NAME = "id";
public String id() default ID_NAME;
public String name();
}
Note: And to get this information at runtime - at runtime we need to use the @Retention annotation with the value RetentionPolicy.RUNTIME.
Create annotation for Field
@Documented
@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public static String BLANK = "";
public String name() default BLANK;
}
Above, I just defined a simple annotation that the attribute corresponds to what field name? Of course, you can create many other attributes to describe the attribute, depending on the support level of the library you do, such as: Key, Default, Extra...
Create model
Build business models and attach that model to the database table through annotation. Here I will create a model named Person
with the corresponding table name t_person
and its attributes.
@Table(id="t_id", name="t_person")
public class Person extends Model {
@Column(name="t_username")
public String username;
@Column(name="t_password")
public String password;
...
// Similar to defining other attributes
}
Get model information
Reflection is a feature in the Java programming language that allows Java programs to inspect themselves - inspect themselves and automatically call classes, methods or attributes ... at run time.
We get information such as table name, primary key, field name ... of the model through the annotation we described above using Reflection.
// The class stores information about the model such as ids or fields
public class ModelManager {
protected Class<? extends Model> type;
protected String id;
protected String name;
// List stores the list of fields
private List<Field> columns = new ArrayList<>();
// Contructor
public ModelManager(Class<? extends Model> type) {
this.type = type;
Table an = type.getAnnotation(Table.class);
if (an != null) {
id = an.id();
name = an.name();
} else {
id = Table.ID_NAME;
name = type.getSimpleName();
}
columns.add(this.getIdField(type));
columns.addAll(this.getColumnFieldsWithoutId(type));
}
// Get primary key
protected Field getIdField(Class type) {
if (Model.class.equals(type)) {
try {
return type.getDeclaredField(SQLiteUtils.ID);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
} else if (type.getSuperclass() != null) {
return getIdField(type.getSuperclass());
}
return null;
}
// Get a list of fields that do not include the primary key with annotation
public List<Field> getColumnFieldsWithoutId(Class<?> type) {
Field[] fields = type.getDeclaredFields();
List<Field> columns = new ArrayList<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
columns.add(field);
}
}
Class<?> parentType = type.getSuperclass();
if(parentType != null) {
columns.addAll(getColumnFieldsWithoutId(parentType));
}
return columns;
}
}
Build operation methods for the model
Reflection also allows us to automatically SET the value for the property automatically without going through the SET method which we will use a lot here.
Saving an object to the database
private final long ID_NOT_SET = -1;
public void save() {
// Get the list of fields of the table
List<Field> columns = table.getColumnFields();
ContentValues values = new ContentValues(table.getSize());
String colName;
for (Field column : columns) {
column.setAccessible(true);
if(SQLiteUtils.ID.equals(column.getName())) {
// In case of insert, the id will be not set
if(id == ID_NOT_SET) continue;
colName = table.id;
} else {
colName = column.getAnnotation(Column.class).name();
}
try {
Object obj = column.get(this);
values.put(colName, (obj == null) ? "" : String.valueOf(obj));
} catch (Exception ex) {
AppLog.log(ex.getLocalizedMessage());
}
}
if (id == ID_NOT_SET) {
id = Database.insert(table.getName(), values);
}
else {
int cnt = Database.update(table.getName(), values, table.id + "= ?", new String[] {String.valueOf(id)});
}
}
Take a record and convert it to the corresponding model
Because the data types that the database supports do not completely map to the data types that the programming language you are using. So here you need to handle SET manually each data type.
public static <T extends Model> T findById(Class<T> type, long id) {
T entity = null;
Cursor c = null;
try {
entity = type.newInstance();
c = Database.query(entity.table.getName(), null, entity.table.id + " = ?", new String[] { String.valueOf(id) });
while (c.moveToNext()) {
entity.getRecord(c);
break;
}
} catch (Exception ex) {
AppLog.log(ex.getLocalizedMessage());
} finally {
if (c != null) {
c.close();
}
}
return entity;
}
protected void getRecord(Cursor c) {
String typeString = null, colName;
for (Field field : table.getColumnFields()) {
field.setAccessible(true);
try {
typeString = field.getType().getName();
colName = (SQLiteUtils.ID.equals(field.getName())) ? table.id : field.getAnnotation(Column.class).name();
if (typeString.equals("java.lang.String")) {
String val = c.getString(c.getColumnIndex(colName));
field.set(this, val.equals("null") ? null : val);
} else if (typeString.equals("short")
|| typeString.equals("java.lang.Short")) {
field.set(this, c.getShort(c.getColumnIndex(colName)));
}
// We do the same with the remaining data types.
...
}
Delete record corresponding to the object being manipulated
public int delete() {
int toRet = Database.delete(table.getName(), table.id + " = ?", new String[] { String.valueOf(id) });
return toRet;
}
Similarly, we also build other operators for the model like:
Other features
In addition to performing basic operations like insert, update, delete... I have built an additional utility that automatically upgrade DB according to sql script - pre-built script. This in my opinion is quite necessary because it helps us to manage the version and avoid serious consequences such as being able to delete all or part of the user's data for some reason.
public void upgradeFromSQLScript(SQLiteDatabase db, int oldVersion, int newVersion) {
int index = 1;
for (List<String> builder : QueryBuilder.getBuilder()) {
if(index > oldVersion && index <= newVersion) {
for (String sql : builder) {
try {
db.execSQL(sql);
} catch (Exception e) {
AppLog.log(e.getLocalizedMessage());
}
AppLog.log("Upgrade from sql script: " + sql);
}
}
index ++;
}
}
// QueryBuilder class
public static List<List<String>> getBuilder() {
List<List<String>> builder = new ArrayList<>();
// Version 1
List<String> ver1 = new ArrayList<>();
String TABLE_PERSON = "CREATE TABLE t_person (t_id INTEGER PRIMARY KEY AUTOINCREMENT, t_username text, t_password text)";
ver1.add(TABLE_PERSON);
builder.add(ver1);
// Version 2
List<String> ver2 = new ArrayList<>();
String ALTER_PERSON = "ALTER TABLE t_person ADD COLUMN dt_modified TIMESTAMP";
ver2.add(ALTER_PERSON);
builder.add(ver2);
// Same for later upgrade versions
...
return builder;
}
Demo
// Insert new record
Person p = new Person("buithanh", "password");
p.save();
Result:
Person: buithanh | password
// Update record
p.password = "12345678";
p.save();
Result:
Person: buithanh | 12345678
// Delete record
p.delete();
List<Person> persons = Model.findByColumn(Person.class, "t_username", "buithanh");
if (persons == null
|| persons.size() <= 0) {
AppLog.log("{buithanh} is not found!");
} else {
AppLog.log("{buithanh} count: " + persons.size());
}
Result:
{buithanh} is not found!
Conclude
The library that I created can only perform very basic operations, does not support complex commands and lacks many accompanying utilities. My source code is public here Active Record-Android
References
https://docs.oracle.com/javase/tutorial/java/annotations/index.html
https://docs.oracle.com/javase/tutorial/reflect/index.html