หน้าเว็บ

วันพฤหัสบดีที่ 17 เมษายน พ.ศ. 2557

TreeModel : java


        ตอนนี้  ผมเจอโปรเจ็คที่โครงสร้างข้อมูล (database) ส่วนใหญ่เป็น Tree ซึ่งทำงานร่วมกับ JPA (Hibernate)  เนื่องจากโครงสร้างส่วนใหญ่เป็น Tree ผมเลยสร้าง TreeModel ที่เป็น standard ขึ้นมา  เพื่อให้ (Entity class) Tree ตัวอื่น ๆ inherit / implement ไปใช้ได้เลย

ปัญหาที่เจอคือ การ query ข้อมูลที่ค่อนข้างช้า  เนื่องจากมันเป็น Tree และทำงานร่วมกับ Relational Database  มันเลยต้อง select from where (query) ตามจำนวน parent ที่มีอยู่ เช่นมี parent อยู่ 200  nodes  มันก็ต้อง query 200 รอบ  ซึ่งผมมองว่า  มันไม่คุ้มเอาซ่ะเลย  ก็เลยคิดหาทางแก้  ว่าจะทำยังไงให้มันเร็วขึ้น  จึงเป็นที่มาของบทความวันนี้ครับ

วิธีแก้ของผม  คือการ load ข้อมูลที่เป็น Tree ทั้งหมด มาไว้บน memory ก่อน  (query ครั้งเดียวเท่านั้น)  แล้วเขียน code จัดเรียงโครงสร้าง Tree เอาเองครับ  (ผมลองแล้ว  ความเร็วคนละเรื่องการเลย) ซึ่งมี code ดังต่อไปนี้

เขียน Standard TreeModel ขึ้นมา 

        ซึ่งโครงสร้าง ข้อมูลมันก็จะเป็น TreeModel ธรรมดาๆ นี่แหล่ะครับ  แต่ว่ามันเป็น Generic
เพื่อให้สามารถ รองรับการทำงานได้หลายๆ แบบ

        ต่อมาคือ static method  ซึ่งที่ผมเปิดเป็น public ไว้  ก็มีแค่ provideStructure เพื่อเอาไว้จัดโครงสร้าง Tree บน memory ครับ  เมื่อมันจัดโครงสร้างเสร็จแล้ว  มันจะ return root ของ Tree ออกมา

        มี method หนึ่งที่น่าสนใจ  คือ public T cloneModel() จะเห็นว่า  ผมละ method getChildren ไว้  เนื่องจากผมใช้ hibernate จึงต้องป้องกันการ query ข้อมูลที่จะเกิดขึ้นทุกครั้ง เมื่อมันอยู่ใน Transaction scope
และหน้าที่อีกอยากของ method นี้คือการป้องกัน Exception นึงที่จะเกิดขึ้น นั่นคือ LazyInitializationException : could not initialize proxy - no Session  ซึ่งผม จะเป็นคน manage ลูกของมันเอง  hibernate ไม่ต้อง เพราะถ้าเรา return Entity ที่อยู่ในสถานะ managed ออกไปจาก Transaction แล้ว  ถ้ามันต้องการดึงข้อมูลลูกมันขึ้นมา แล้ว query ไม่ได้  มันก็จะเกิด Exception ที่ว่านั่นแหล่ะครับ  เพราะมันปิด session query ไปแล้ว  คนใช้ hibernate จะเข้าใจครับ  ผมเลยแก้ปัญหาด้วยการ clone model  ซ่ะ (return model ธรรมดาๆ ออกไปแทน managed model)

TreeModel.java
package com.blogspot.na5cent.model;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author redcrow
 * @param <T>
 */
public abstract class TreeModel<T extends TreeModel> {

    private static final Logger LOG = LoggerFactory.getLogger(TreeModel.class);

    public abstract String getId();

    public String getPrefixName() {
        String[] split = StringUtils.split(this.getName(), " ");
        if (split == null || split.length == 0) {
            return "";
        }

        return isPrefix(split[0]) ? split[0] : "";
    }

    private boolean isPrefix(String text) {
        return text.contains(".") || text.contains(")") || containsNumber(text);
    }

    private boolean containsNumber(String text) {
        return text.matches(".*[\\d]+.*");
    }

    public abstract String getName();

    public String getNameNotIncludePrefix() {
        return this.getName().replace(this.getPrefixName(), "");
    }

    public abstract T getParent();

    public abstract void setParent(T parent);

    public abstract List<T> getChildren();

    public void addChild(T child) {
        if (child == null) {
            return;
        }

        child.setParent(this);
        this.getChildren().add(child);
    }

    public void setChildren(List<T> children) {
        if (children == null) {
            return;
        }

        this.getChildren().clear();
        for (T child : children) {
            this.addChild(child);
        }
    }

    /**
     * for clone all implementation of TreeModel
     *
     * @return T
     * @throws Exception
     */
    public T cloneModel() throws Exception {
        Class<? extends TreeModel> clazz = this.getClass();
        T instance = (T) clazz.newInstance();
        Method[] methods = clazz.getDeclaredMethods();

        for (Method method : methods) {
            String name = method.getName();
            /*
             not include getChildren for protect
             -------------------------------------------------------------------
             Hibernate: org.hibernate.LazyInitializationException: could not initialize proxy - no Session
             */
            if (name.startsWith("get") && !name.equals("getChildren")) {
                name = name.replace("get", "");

                Method getMethod = clazz.getDeclaredMethod("get" + name);
                Method setMethod = clazz.getDeclaredMethod("set" + name, method.getReturnType());

                setMethod.invoke(instance, getMethod.invoke(this));
            }
        }

        return instance;
    }

    public static <T extends TreeModel> T provideStructure(List<T> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }

        T root = findChildByParent(list, null);
        walking(list, root);

        return root;
    }

    private static <T extends TreeModel> void walking(List<T> list, T parent) {
        if (parent == null) {
            return;
        }

        List<T> children = findChildrenByParent(list, parent);
        if (children.isEmpty()) {
            return;
        }

        for (T child : children) {
            walking(list, child);
        }

        parent.setChildren(children);
    }

    private static <T extends TreeModel> List<T> findChildrenByParent(List<T> list, T parent) {
        List<T> children = new ArrayList<>();
        while (true) {
            T child = findChildByParent(list, parent);
            if (child == null) {
                break;
            }

            children.add(child);
        }

        return children;
    }

    private static <T extends TreeModel> T findChildByParent(List<T> list, T parent) {
        T child = null;
        if (parent == null) {
            for (T model : list) {
                if (null == model.getParent()) {
                    child = model;
                    break;
                }
            }
        } else {
            for (T model : list) {
                if (parent.equals(model.getParent())) {
                    child = model;
                    break;
                }
            }
        }

        if (child == null) {
            return null;
        }

        list.remove(child);
        return cloneModel(child);
    }

    private static <T extends TreeModel> T cloneModel(T model) {
        T clone;

        try {
            clone = (T) model.cloneModel();
        } catch (Exception ex) {
            clone = null;
            LOG.warn(null, ex);
        }

        return clone;
    }
}
การนำ TreeModel ไปใช้ 
Personnel.java (สืบทอดพฤติกรรมมาจาก TreeModel)
...
...
...
@Entity
@Table(name = "personnel")
public class Personnel extends TreeModel<Personnel> { /*****/

    @Id
    private String id;
    @Version
    private Integer version;

    @Column(columnDefinition = "NVARCHAR2(255)")
    private String name;
    private String type;

    @ManyToOne
    private Personnel parent;
    @OneToMany(mappedBy = "parent")
    private List<Personnel> children;

    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
...
...
...
Service สำหรับ load ข้อมูล
PersonnelServiceImpl.java (load ข้อมูล Tree ทั้งหมดมาจัดโครงสร้างเองบน memory)
...
...
...
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class PersonnelServiceImpl implements PersonnelService {

    @Autowired
    private PersonnelRepository personnelRepository;

    @Override
    public Personnel findRoot() {
        List<Personnel> personnels = personnelRepository.findAll();
        return TreeModel.provideStructure(personnels); /*****/
    }
}



หมายเหตุ : แน่นอนว่า กรรมวิธีแต่ละอย่างล้วนมีทั้งข้อดีและข้อเสีย  ซึ่งวิธีนี้ค่อนข้างที่จะเปลือง memory นิดหน่อยครับ  แต่ก็ได้มาซึ่งความเร็ว

ส่วนเราจะตัดสินใจใช้วิธีไหนนั้น  มันก็ต้องขึ้นอยู่กับความเหมาะสมของปริมาณข้อมูลที่มีอยู่ด้วย  ไม่ใช่ว่ามี Tree เป็น แสน เป็น ล้าน nodes แล้ววิธีนี้จะเป็นวิธีที่ดีที่สุดครับ

ที่มาของภาพประกอบ http://treeremovalpittsburgh.org/

ไม่มีความคิดเห็น:

แสดงความคิดเห็น