วันศุกร์ที่ 24 มิถุนายน พ.ศ. 2554

Hibernate Annotation generate id from oracle function

แก้ไขการ Generate ID จาก ORACLE Sequence มาเป็น เรียก ORACLE Function
แนะนำความต้องทำความรู้จัก Hibernate, Spring Framework เบื้องต้น ก็จะเป็นการดีครับผม

- spring framework 3 quickstart
- Spring Framework and Hibernate on Annotation

เรามาเข้าจุดที่เราสนใจกันจริง ๆ ดีกว่า ในส่วนของ Hibernate Map Class Model กับ Database นั้น
เราจะมีการเขียน Class ในลักษณะนี้ครับ

โดยในตัวอย่างแรกนี้ จะเป็นเรื่องการ Map ID ของ Class ให้มีการดึง Sequence Oracle
มาเป็น id ตอน insert ข้อมูล

Process.java
package x.y.z.projecttracking.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name="PROCESS")
public class Process extends BaseEntity {
    
    public Process(){}
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_PROJ_GEN")
    @SequenceGenerator(name = "SEQ_PROJ_GEN", sequenceName = "SEQ_PROJ")
    private Long id;
    
    @Column
    private String code;
    
    @Column
    private String description;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
    
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

ในส่วนนี้จะเป็นเครื่องมือที่ Hibernate มีให้ใช้งานได้ทันที

ส่วนที่สำคัญที่มีผลต่อการ generate id ก็เป็นดังนี้

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_PROJ_GEN")
ในส่วน GeneratedValue นี้จะบอกชนิดของการสร้าง id โดยระบุ strategy คือ ลักษณะการสร้าง id เป็น
Sequence Oracle
(NOTE : กรณี MySQL นั้น ไม่มี Sequence จะใช้แค่ @GeneratedValue เปล่า ๆ เลยแล้วทาง MySQL จะรู้เองว่า ต้องการขอ id ใหม่ ก็จะสร้างเลขใหม่ไม่ซ้ำให้ แต่ ORACLE ไม่มี auto generate เช่นนั้น โดยทั่วไปถึงต้องเรียกใช้ sequence ที่เป็นอีก function เฉพาะใน oracle ให้ทำการสร้าง running -number เพื่องานใด ๆ)

ส่วน generator นั้นก็จะเป็นตัวบอกว่าไปหา ตัวที่ทำการสร้างนั้นที่ใหน ด้วยชื่อ ตามด้านล่างนี้เลยครับ

@SequenceGenerator(name = "SEQ_PROJ_GEN", sequenceName = "SEQ_PROJ")
ในส่วน SequenceGenerator ตัวนี้ถือเป็นเครื่องมือในการดึงข้อมูลจาก Sequence ORACLE ที่เราได้เตรียมไว้แล้ว
โดย name ก็คือชื่อแทนตัว ไว้ให้สำหรับเรียกใช้จากที่อื่น
ส่วน sequenceName ก็จะเป็นตัวระบุว่า Sequence ORACLE ที่เราต้องการใช้งานนั้น มีชื่อว่าอะไร

จบในส่วน เกรินนำ ID Generate By Sequence ไว้เท่านี้ก่อน

เราไปเริ่มแก้ให้เป็น ID Generate By Function กันดีกว่า

เริ่มจาก เราเปลี่ยน Generator ใหม่เป็นดังนี้

Process.java
package x.y.z.projecttracking.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name="PROCESS")
public class Process extends BaseEntity {
    
    public Process(){}
    
    @Id
    @GeneratedValue(generator = "projIdSeqGen")
    @GenericGenerator(
        name = "projIdSeqGen", 
        strategy = "x.y.z.projecttracking.generator.ProjIdSequenceGenerator")
    private Long id;
    
    @Column
    private String code;
    
    @Column
    private String description;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
    
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

มีส่วนที่เปลี่ยนแปลง 2 ส่วน ได้แก่
@GeneratedValue(generator = "projIdSeqGen")
ในส่วน strategy เราไม่ต้องระบุแล้ว เพราะเราจะปรับแต่งเอาเอง โดยใช้ตัว genreator พื้นฐานคือ GenericGenerator

@GenericGenerator(
name = "projIdSeqGen",
strategy = "x.y.z.projecttracking.generator.ProjIdSequenceGenerator")
ในส่วนนี้เราใช้ GenericGenerator แล้วนำมาปรับแต่ง เพิ่มเติมโดย เราจะเห็นว่ามีการระบุ strategy ขึ้นมา
แล้วเราก็เขียน class ซึ่งจะมี code ตามด้านล่างนี้

package x.y.z.projecttracking.generator;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.CallableStatement;
import java.sql.Types;
import java.util.Properties;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.id.Configurable;
import org.hibernate.id.PersistentIdentifierGenerator;
import org.hibernate.id.SequenceHiLoGenerator;
import org.hibernate.type.Type;

/**
 *
 * @author Kan.s
 */
public abstract class FunctionIdSequenceGenerator implements PersistentIdentifierGenerator, Configurable {

    protected final SequenceHiLoGenerator delegate = new SequenceHiLoGenerator();
    protected Constructor constructor;

    @Override
    public String[] sqlCreateStrings(Dialect dialect) throws HibernateException {
        return delegate.sqlCreateStrings(dialect);
    }

    @Override
    public String[] sqlDropStrings(Dialect dialect) throws HibernateException {
        return delegate.sqlDropStrings(dialect);
    }

    @Override
    public Object generatorKey() {
        return delegate.generatorKey();
    }

    @Override
    public Serializable generate(SessionImplementor session, Object object) throws HibernateException {
        try {
            String sqlCallFunc = "{ ? = call "+getFunctionName()+"() }";
            System.out.println("SQL call Function() : " + sqlCallFunc);
            CallableStatement call = session.connection().prepareCall(sqlCallFunc);
            call.registerOutParameter(1, Types.VARCHAR);
            call.execute();
            String result = call.getString(1);
            return typedIdForValue(result);
        } catch (Exception e) {
            throw new HibernateException("delegate didn't return a string", e);
        }
    }

    @Override
    public void configure(Type type, Properties params, Dialect d) throws MappingException {
        delegate.configure(Hibernate.STRING, params, d);
        try {
            constructor = type.getReturnedClass().getConstructor(String.class);
        } catch (NoSuchMethodException e) {
            throw new HibernateException("FunctionIdSequenceGenerator is only applicable for types deriving from AbstractPersistenLongId", e);
        }
    }

    protected Serializable typedIdForValue(String val) {
        try {
            return (Serializable) constructor.newInstance(val);
        } catch (InstantiationException e) {
            throw new HibernateException("Cannot create strongly typed id", e);

        } catch (IllegalAccessException e) {
            throw new HibernateException("Cannot create strongly typed id", e);

        } catch (InvocationTargetException e) {
            throw new HibernateException("Cannot create strongly typed id", e);
        }
    }

    public abstract String getFunctionName();
}

class นี้ สร้างเป็น abstract เพื่อให้สามารถนำไปเรียกใช้ด้วย class อื่นได้ ไม่ได้ระบุว่า class นี้นำไปทำงานได้ทันทีเนื่องจากอยากให้สามารถใช้ประโยชน์ได้หลายครั้ง ไม่ต้องเขียนใหม่บ่อย

id ส่วนนี้ ผมจะ return String เลยจัดการแก้ในส่วน Method configure()

ส่วนสำคัญที่ทำหน้าที่สร้าง ID คือ Method generate ในส่วนนี้ผมได้แก้ให้เป็น เรียก Function ORACLE โดย Class ที่นำไปใช้งานจริง จะถูกบังคับให้สร้าง Method getFunctionName() เพื่อระบุชื่อ Function ORACLE ที่ต้องการทำการเรียกใช้งาน

หากต้องการใส่ parameter เข้าไปยัง function ก็ใช้ประมาณนี้ครับ
    String sqlCallFunc = "{ ? = call "+getFunctionName()+"(?, ?) }";
    String param1 = "test";
    Long param2 = "test";
    CallableStatement call = session.connection().prepareCall(sqlCallFunc);
    call.registerOutParameter(1, Types.VARCHAR);
    call.setString(2, param1);
    call.setLong(3, param2 );
    call.execute();
    String result = call.getString(1);
ตามตัวอย่างนี้ก็หวังว่าจะนำไป apply ใช้ประโยชน์ได้นะครับ

ส่วน Class ที่นำไปใช้งานจริงก็เป็นแบบนี้ครับ

package x.y.z.projecttracking.generator;

/**
 *
 * @author Kan.s
 */
public class ProjIdSequenceGenerator extends FunctionIdSequenceGenerator {

    @Override
    public String getFunctionName() {
        return "getProjId";
    }
}

เรามาดูตัวอย่าง PL/SQL ที่ Generate function ORACLE กันบ้างดีกว่า
ในที่นี้เขียนง่าย ๆ เพราะเขียนได้แค่นี้ครับ T_T
ขั้นตอน คือ นำเอาวันเวลาปัจจุบัน มาแล้วเติม prefix เป็น 'PJ'
CREATE OR REPLACE FUNCTION getProjId(
) RETURN VARCHAR2 IS
    dttm_curr VARCHAR2(20);
    cursor c1 IS 
    SELECT TO_CHAR(CURRENT_DATE, 'DDMMYYYYHHMISSSSS') FROM DUAL;
BEGIN
   open c1;      
   fetch c1 into dttm_curr;
   close c1;
   return CONCAT('PJ', dttm_curr); 
END getId4DttmCurr  ;

Result : PJXXXXXXXXXXXXXXXXX
X is number 0-9


กรณี เขียนแบบให้รับ parameter
CREATE OR REPLACE FUNCTION getProjId(prefix IN varchar2
) RETURN VARCHAR2 IS
    dttm_curr VARCHAR2(20);
    cursor c1 IS 
    SELECT TO_CHAR(CURRENT_DATE, 'DDMMYYYYHHMISSSSS') FROM DUAL;
BEGIN
   open c1;      
   fetch c1 into dttm_curr;
   close c1;
   return CONCAT(prefix, dttm_curr); 
END getId4DttmCurr  ;

------------------------------------------------
Thank you for...
credit :
- Custom Id Generators For Typed Id Values With Hibernate
- stackoverflow : How to call a Oracle function from hibernate with return parameter?

วันศุกร์ที่ 17 มิถุนายน พ.ศ. 2554

what ? difference for 'Action' and 'ActionListener' in JSF

action จะ return outcome ซึ่งจะมีผลให้ระบบ navigator system ทำงาน
ว่าทำ action นี้แล้วไปที่ view ไหน (โดยดูจาก outcome ) คล้ายๆกับ findforward ใน Struts น่ะครับ

ส่วน actionListener นั้นเป็น event handler จากการทำ action นี้น่ะครับ
ไม่มีการเปลี่ยน view

อีกอย่าง method signature ของทั้งสองจะต่างกันด้วย โดยตัว method ของ actionListener นั้นจะมี argument ที่เป็น ActionEvent ด้วย เช่น

public void doIt(ActionEvent event){
  ...
}

method signature ของ action นั้นจะไม่มี argument แต่จะ return String ที่เป็น outcome ของ action ออกมาแทน เช่น

public String login() {
  if ( ... ) // login is successful {
    return "success";
  } else {
    return "failure";
  }
}

Note:
actionListener ถูกทำก่อนครับ แล้ว action จะถูกทำทีหลัง

Credit:
http://www.narisa.com/forums/index.php?showtopic=7176

วันพฤหัสบดีที่ 16 มิถุนายน พ.ศ. 2554

JSF2 and Internationalization

ขอรวบรัดนิดนึงครับ ว่าจะเอา ตัวอย่างมาจากบทก่อน เพื่อแก้เพิ่มเนื่องจากเวลาจำกัด
ให้ดูตัวอย่างเริ่มต้นจากด้านล่างนี้ครับ แล้วเราจะมาเพิ่มเติมกันเลย

JSF 2.0 and Spring Framework annotation example

เริ่มแรกให้เราไปเพิ่ม locale และ path file properties ให้ app เรารู้จักก่อน โดยไปเพิ่มที่ file
faces-config.xml
<application>
 <locale-config>
  <default-locale>en</default-locale>
  <supported-locale>th</supported-locale>
 </locale-config>
 <resource-bundle>
  <base-name>message</base-name>
  <var>msg</var>
 </resource-bundle>
</application>

จากตัวอย่าง code ก็จะอธิบายสั้น ๆ ว่า เรากำหนด msg เพื่อให้สะดวกเรียกใช้บนหน้า jsf โดยเวลาเราเรียกใช้จะได้ดังนี้
#{msg['label.username']}

เราจะเพิ่ม file ที่ชื่อ message.properties (มี default locale เป็น en) และ file ที่ชื่อ message_th.properties ทั้ง 2 file นี้จะสร้างไว้ที่ classpath
โดยจะกำหนดค่าในแต่ละ file ดังนี้

message.properties
label.username=User Name
label.password=Password

message_th.properties
label.username=ชื่อผู้ใช้
label.password=รหัสผ่าน

ส่วนต่อมาคือ code ส่วนที่นำไปแปะไว้ที่หน้า web jsf file เพื่อกด click แล้วให้ทำการสลับ ภาษาในหน้านั้น
<h:form>
 <h:commandLink actionListener="#{localizationBean.changeLanguage('th')}" >
  <h:graphicImage value="/images/icon_flag/th.png" styleClass="pic" />
 </h:commandLink>
 / 
 <h:commandLink actionListener="#{localizationBean.changeLanguage('en')}" >
  <h:graphicImage value="/images/icon_flag/en.png" styleClass="pic" />
 </h:commandLink>
</h:form>

ในที่นี้มี h:form เพื่อทำการ refresh ค่าในหน้า page
ตัวอย่างจาก code ก็คือ มีธงชาติ 2 ประเทศ กดอันใหน ก็เป็นภาษานั้น เข้าใจง่ายดี รู้ไม่มีจินตนาการเอาเอง
โดยปกติ แล้วก็ควรจะวางไว้บน header เพื่อที่จะได้ reuse ได้

ส่วนต่อมาคือ class ที่เป็น bean ทำหน้าที่ set locale อยู่เบื้องหลัง code ค่อนข้างจะน้อย ที่สำคัญคือ
ให้ set bean scope เป็น session ไม่งั้น ค่ามันจะหลุดจากการ set locale ครั้งล่าสุด ไปเป็นค่า defaultตลอด
package x.y.z.project.web.jsf.bean;

import java.io.Serializable;
import java.util.Locale;
import javax.faces.context.FacesContext;
import javax.inject.Named;
import org.springframework.context.annotation.Scope;

@Named("localizationBean")
@Scope("session")
public class LocalizationBean implements Serializable {
    
    private Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();

    public void changeLanguage(String language) {
        locale = new Locale(language);
        FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);
    }

    public Locale getLocale() {
        return locale;
    }

    public String getLanguage() {
        return locale.getLanguage();
    }
}

ต่อไปก็เป็นส่วนที่ใช้งานบนหน้า web จริง คือส่วนที่ถูกเปลี่ยนแปลง ดูตัวอย่างเลยดีกว่า
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a4j="http://richfaces.org/a4j"
      xmlns:rich="http://richfaces.org/rich"
      xmlns:ui="http://java.sun.com/jsf/facelets">
    <f:view locale="#{localizationBean.locale}">
  <h:head>
   <title>Project</title>
  </h:head>
  <h:body>
   <f:facet name="header">
    <ui:include src="header.xhtml" />
   </f:facet>

   <rich:panel>
   <h:panelGrid columns="2" styleClass="formhilightlefttable">
    <h:outputLabel value="#{msg['label.username']}" />
    <h:inputText value="#{loginBean.username}"/>
    
    <h:outputLabel value="#{msg['label.password']}" />
    <h:inputText value="#{loginBean.password}"/>
   </h:panelGrid>
   </rich:panel>
  </h:body>
 </f:view>
</html>

ในส่วนนี้ที่สำคัญคือ
ที่ไว้ใช้กำหนด เก็บค่า locale ไว้สำหรับทั้งหน้า

#{msg['label.username']} เป็นตัวอย่างค่าที่ดึงจาก file message.properties ในส่วนนี้จะถูกเปลี่ยน ค่าไป จาก ไทย เป็น อังกฤษ เมื่อมีการคลิ๊กที่ธงชาติ

#{loginBean.username} ตรงนี้เป็นในส่วนติดต่อ กับ bean หลังบ้าน เพื่อใช้เก็บค่า และดำเนินการต่อไป

ui:include src="header.xhtml" ในส่วนนี้ได้ทำการ include file header มาส่วนใหญ่แล้ว ก็จะวางในส่วน menu ต่าง ๆ ไว้ รวมถึง locale ที่เป็นรูปธงไว้ใช้ สลับภาษาไว้บน file นี้ด้วย