Làm sao để xây dựng thư viện ORM

Tổng quan

ORM hay Object Relational Mapping: Là một kĩ thuật cho phép bạn truy vấn và thao tác dữ liệu trên database bằng cách sử dụng mô hình hướng đối tượng. Thông qua đó cho phép bạn thao tác trực tiếp với data trên database thông qua đối tượng chứ không phải câu lệnh SQL.

Dưới đây là sơ đồ hoạt động của ORM.

alt text

Sau đây là ví dụ khi không sử dụng ORM

String sql = "INSERT INTO t_person VALUES (username=\"buithanh\",  password=\"password\")";
database.exeSQL(sql);

và khi sử dụng ORM

Person p = new Person("buithanh", "password");
p.save();
Ưu điểm
  • Không cần phải viết SQL
  • Được cung cấp sẵn các tiện ích tuỳ vào từng thư viện ORM như: Tự động nâng cấp database hay quản lý các transaction...
Nhược điểm
  • Bạn phải cài đặt và tìm hiểu cách sử dụng thư viện ORM
  • Hiệu năng của nó là tốt cho các truy vấn thông thường nhưng sẽ không hiệu quả so bằng sử dụng SQL ở những big query.
Một số thư viện ORM
  • Java: Hibernate
  • Objective C: Core Data
  • PHP: CakePHP, Laravel
  • Python: Django ORM, Storm
  • Ruby: ActiveRecord, DataMapper

Xây dựng thư viện ORM

Trong bài viết này mình chỉ giới thiệu cách xây dựng thư viện ORM trên ngôn ngữ Java và platform là Android mà thôi.

Như chúng ta đã biết bất cứ một CSDL quan hệ nào đều được cấu thành bởi các table và table bao gồm các record lưu thông tin về một entity - thực thể nào đó. Còn với OOP thì mọi thứ đều là đối tượng và đối tượng cũng là đơn vị lưu trữ thông tin giống như record trong CSDL quan hệ.

Do đó ta thấy có một sự tương ứng rằng class name tương đương vs table name, một đối tượng tương đương với một record và các attribute của nó tương đương với các field.

Và sau đây là bước để xây dựng thư viện:

1. Tạo annotation type mới

Annotation - chú thích: Là một dạng metadata cung cấp thông tin về chương trình và nó không có ảnh hưởng trực tiếp đến code mà nó chú thích. Đối tượng có thể sử dụng annotation là: classes, methods, fields, parameters và packages.

Do đó bằng cách nào đó thông qua anotation chúng ta có thể biết được class đó tương ứng vs table nào? attribute đó tương ứng với field nào? và các field có thuộc tính gì? chẳng hạn "PRIMARY KEY", "NOT NULL", "UNSIGNED"...

Tạo annotation cho 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();
}

Lưu ý: Và để lấy được thông tin này ở runtime - thời điểm chạy ta cần sử dụng annotation @Retention với giá trị là RetentionPolicy.RUNTIME.

Tạo annotation cho Field
@Documented
@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)

public @interface Column {
    public static String BLANK = "";
    public String name() default BLANK;
}

Ở trên mình chỉ định nghĩa một annotation đơn giản rằng attribute đó tương ứng với field name gì? Tất nhiên bạn có thể tạo ra rất nhiều các thuộc tính khác để mô tả attribute nữa tuỳ vào mức độ hỗ trợ của thư viện mà bạn làm như: Key, Default, Extra...

2. Tạo model

Xây dựng các business model và gắn model đó với table của CSDL thông qua annotation. Sau đây mình sẽ tạo một model có tên là Person với table name tương ứng là t_person và các attribute của nó.

@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;
    ...
    // Tương tự định nghĩa các attribute khác
}

3. Lấy thông tin của model sử dụng Reflection

Reflection: Là một tính năng trong ngôn ngữ lập trình Java nó cho phép các chương trình Java tự kiểm tra - inspect chính nó và tự động gọi các class, method hay attribute ... ở thời điểm thực thi.

Chúng ta thực hiện lấy thông tin như table name, primary key, field name ... của model thông qua annotation mà chúng ta đã mô tả ở trên sử dụng Reflection.

// Class lưu trữ thông tin về model như id hay các field
public class ModelManager {
    protected Class<? extends Model> type;
    protected String id;
    protected String name;

    // List lưu danh sách các field
    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));
    }

    // Lấy 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;
    }

    // Lấy danh sách field không bao gồm primary key có anotation
    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;
    }
}

4. Xây dựng các phương thức thao tác cho model

Reflection cũng cho phép chúng ta tự động SET giá trị cho thuộc tính tự động mà không cần thông qua SET method mà ta sẽ sử dụng nhiều ở đây.

Lưu một đối tượng xuống CSDL
private final long ID_NOT_SET = -1;

public void save() {
    // Lấy danh sách field của 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())) {
            // Trường hợp insert thì id sẽ là 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)});
    }
}
Lấy một record và convert nó sang model tương ứng

Bởi vì các kiểu dữ liệu mà CSDL support không hoàn toàn map với các kiểu dữ liệu mà ngôn ngữ lập trình bạn đang sử dụng. Do đó ở đây bạn cần phải tự xử lý SET bằng tay từng kiểu dữ liệu.

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)));
        }
        // Chúng ta thực hiện tương tự với các kiểu dữ liệu còn lại.
        ...
}

Delete record ứng với đối tượng đang thao tác
public int delete() {
    int toRet = Database.delete(table.getName(), table.id + " = ?", new String[] { String.valueOf(id) });
    return toRet;
}

Tương tự chúng ta cũng xây dựng các operator khác cho model như:

  • findByIds(Class type, long[] ids)
  • find(Class type, String whereClause, String[] whereArgs)
  • findByColumn(Class type, String column, String value)
  • deleteByIds(Class type, long[] ids)

5. Các tiện ích đi kèm

Ngoài thực hiện các thao tác cơ bản như insert, update, delete... mình có xây dựng thêm tiện ích bổ xung là tự động upgrade DB theo sql script - kịch bản tạo sẵn. Cái này theo mình là khá cần thiết bởi nó giúp chúng ta quản lý version và tránh những hậu quả nặng nề như có thể xoá toàn bộ hay một phần data của user vì lý do nào đó.

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);

    // Tương tự cho các version upgrade sau đó
    ...

    return builder;
}

6. Demo

// insert new record
Person p = new Person("buithanh", "password");
p.save();

Kết quả:
Person: buithanh | password

// update record
p.password = "12345678";
p.save();

Kết quả:
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());
}

Kết quả:
{buithanh} is not found!

Kết luận

Thư viện mà mình tạo được mới chỉ thực hiện được những thao tác rất cơ bản, chưa hỗ trợ những command phức tạp cũng như thiếu nhiều tiện ích đi kèm. Do đó bạn nào có ý tưởng hay cách làm nào thú vị thì chia sẻ mình nhé.

Source code mình public tại đây Active Record-Android

Tài liệu tham khảo

https://docs.oracle.com/javase/tutorial/java/annotations/index.html/
https://docs.oracle.com/javase/tutorial/reflect/index.html/

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

bqthanh

5 bài viết.
7 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
20 0
Tổng quan về AWS Amazon web services là một nền tảng điện toán đám mây được phát triển và cung cấp bởi Amazon. Regions and Availability Zones C...
bqthanh viết 1 năm trước
20 0
White
6 1
Tổng quan DNS Domain Name System hay hệ thống tên miền là một cơ sở dữ liệu phân tán nằm trên các server khác nhau lưu thông tin ánh xạ giữa domai...
bqthanh viết 8 tháng trước
6 1
White
2 1
Architecture Pattern Là một tập hợp các quy tắc để giải thích chúng ta có những class nào? chúng sẽ tương tác với nhau ra sao để thực hiện một hệ ...
bqthanh viết 10 tháng trước
2 1
Bài viết liên quan
White
0 0
Mình khá là lười, nên mình sẽ không đưa định nghĩa hay usage của reflection vào đây. Vì dù sao mình cũng sẽ chỉ copy thôi :smile:. Vậy nên chúng ta...
Rice viết 11 tháng trước
0 0
White
0 0
Trong bài viết này, một số hình ảnh hoặc nọi dung có thể bị thiếu do quá trình chế bản. Vui lòng xem nội dung ở blog gốc sau: (Link) (Link), chúng...
programmerit viết 4 năm trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
5 bài viết.
7 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!