2013年8月25日 星期日

MyBatis - 如何一次取得SELECT 的大量資料

參考:Result Maps
需求:我們常常會有SELECT Query回傳大量資料列的情形,與之前指定userid只回傳單一User不同,該如何做呢?

方式很簡單,首先我們新增一個新的function getAllUsers:


  • UserMapper.xml
    
     
      
       
       
      
    
    
      < select id="getAllUsers" resultMap="userMap">
        SELECT 
          userid , 
          email , 
          Password, 
          name
         FROM USER 
     < select>
    
    

    (原有getUserById在這邊先拿掉)請注意其中我們使用的屬性為resultMap而不是先前getUserById中使用的resultType,依照官方說明,resultMap與resultType只能選其中一個使用


  • UserDAO.java
    public class UserDAO {
       //...
        public static List getAllUsers(SqlSessionFactory sqlSessionFactory) {
     SqlSession sqlSession = sqlSessionFactory.openSession();
     try {
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      return userMapper.getAllUsers();
     } finally {
      sqlSession.close();
     }
        }
    }
    

    跟之前使用getUserById()沒甚麼差異,唯一差別就是回傳型態變成了 List<user>
  • 呼叫方式如下:
     private static void getAllUsers(SqlSessionFactory sqlSessionFactory){
      List list = UserDAO.getAllUsers(sqlSessionFactory);
      for(User user : list)
       System.out.println(user);
     }
    
  • 使用方式非常簡單呢,比起以往自己寫JDBC API為了接收大量資料寫了一大堆Code又不好用,ResultMap的機制真的非常簡易又方便呢!

    MyBatis - 使用resultMap(1)


    這這一篇MyBatis - 基本安裝 + 使用Select有提過,mybatis使用了reflection的機制對應Field Name將DataBase的資訊映射至指定Class Data(User.class)中,那麼,如果像我習慣把class field name前面加上 _ 或 m_ 的識別符號,如下:
    public class User {
     private String _userid;
     private String _name;
     private String _password;
     private String _email;
    

    但是在SELECT Query依然維持:
         SELECT 
          userid as userid, 
          email as email , 
          password, 
          name as Name
         FROM USER 
    

    該怎麼做呢?

    需求: 我們需要一次回傳大量的SELECT資料,該怎麼做呢?

    這時候就要使用resultMap的機制了,我們將先前的UserMapper.xml更改為如下:
    
    
      < select id='getUserById' parameterType='String' resultType='User'>
         SELECT 
          userid as Userid, 
          email as Email , 
          Password, 
          name as Name
         FROM USER 
         WHERE USERID = #{userId}
      </ select>
    
    
      
       
       
      
    
      
    
    

    在這裡,我們加上了< resultmap></ resultmap>的設定,依照上面設定很容易可以看得出來,
    column代表的是SELECT 出來的 field name,而對應的property代表的就是User Class中的field name,如此一來,利用這個方式就不需要名字一定得相同囉。


    2013/9/7補充:
      此種方式只適用一次回傳大量資料的resultMap,並不適用SELECT單一資料的resultType,
    如果有需要,還是得乖乖指定回傳fieldName,詳情請見MyBatis - 使用resultType回傳單一SELECT資料時都是Null

    Java Reflection - 存取Field 資訊

    參考:Java 學習筆記 (10) - Reflection

    因為摸了MyBatis(請見MyBatis - 基本安裝 + 使用Select)後對於他可以利用Xml可以做各種設定以及回傳對應Data的方式相當感興趣,直覺是使用Reflection機制實做,因此有了這篇小實驗 :)

    需求:假設我們有一個User Class如下,我們需要能
    • 需求1 - 取得指定所有Public的Field資訊
    • 需求2 - 取得指定指定的Declare的Field資訊(包含private、protected)
     
    public class User {
     private String userid;
     private String name;
     private String password;
     private String email;
      //...other
    }
    

    1. 取得指定Class中所有Public的Field資訊(由於以上的User Class沒有任何public field,因此只列出使用方式)
          Class aClass = User.class;
          Field[] publics = aClass.getFields();
      
    2. 取得指定指定的Declare的Field資訊(包含private、protected)
      假設我們現在想要取得其中的userid與email,並對他其中一個已存在的實體Instance做修改,怎麼做?:
      • 取得User Class的useir、email Field:
          Class aClass = User.class;
          Field fuserid = aClass.getDeclaredField("userid"); 
          Field femail = aClass.getDeclaredField("email"); 
          fuserid.setAccessible(true);
          femail.setAccessible(true);
        
        由於我們希望能修改其中的內容,因此記得要呼叫setAccessible(true),讓該field可以被修改
      • 對已建立的實體Instance進行修改:
           Constructor cons = aClass.getConstructor(null);
           User u = (User) cons.newInstance(null);
           fuserid.set(user, "ifAfter");
           femail.set(user, "mail@After.com");

    以下為完整範例:

  • User.java
    package cmpnts;
    
    public class User {
     private String userid;
     private String name;
     private String password;
     private String email;
    
     public String getUserId() {
      return userid;
     }
    
     public void setUserId(String userid) {
      this.userid = userid;
     }
    
     public String getName() {
      return name;
     }
    
     public void setName(String username) {
      this.name = username;
     }
    
     public String getPassword() {
      return password;
     }
    
     public void setPassword(String pwd) {
      this.password = pwd;
     }
     
     public String getEmaill(){
      return email;
     }
     
     public void setEmaill(String mail){
      email = mail;
     }
     
     @Override
     public String toString(){
      StringBuffer buf = new StringBuffer();
      buf.append("UserId:").append(this.getUserId());
      buf.append("\r\n\tname:").append(this.getName());
      buf.append("\r\n\tpassword:").append(this.getPassword());
      buf.append("\r\n\temail:").append(this.getEmaill());
      return buf.toString();
     }
    }
    
  • FieldReflectionTest.java:
    package reflection;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    
    import cmpnts.User;
    
    public class FieldReflectionTest {
    
     public static void main(String[] args) {
      User user = new User();
      user.setUserId("idBefore");
      user.setName("name");
      user.setPassword("password");
      user.setEmaill("mail@before.com");
      
      try {
       alterFieldUserIdEmail(user);
      } catch (Exception e) {
       e.printStackTrace();
      }
     }
     
     public Field[] getPublicFields(){
      Class aClass = User.class;
      Field[] publics = aClass.getFields();
      return publics;
     }
     
     public static void alterFieldUserIdEmail(User user) throws Exception{
      System.out.println(user);
      
      Class aClass = User.class;
      Field fuserid = aClass.getDeclaredField("userid"); 
      Field femail = aClass.getDeclaredField("email"); 
      fuserid.setAccessible(true);
      femail.setAccessible(true);
      
      Constructor cons = aClass.getConstructor(null);
      User u = (User) cons.newInstance(null);
      fuserid.set(user, "idAfter");
      femail.set(user, "mail@After.com");
      System.out.println(user);
     }
    
    }
    


  • 執行結果:
    UserId:idBefore
    name:name
    password:password
    email:mail@before.com

    UserId:idAfter
    name:name
    password:password
    email:mail@After.com

    修改成功!!

    MyBatis - 基本安裝 + 使用Select

    1. 於eclipse建立New Project【TestMyBatis】
    2. eclipse中的BuildPath加入mybatis-xxxx.jar(我安裝這時候為mybatis-3.2.2.jar)
    3. 建立res資料夾,這個資料夾會存放所有mybatis的config、mapper...等XML設定
    4. 於eclipse的Build Path加入res路徑(Add Folder->選擇res資料夾)

    5. 於res下加入jdbc.properties,這是讓我們設定jdbc資料庫連線的各種設定;這邊我們使用hsqldb,方便測試使用:
      jdbc.driver=org.hsqldb.jdbcDriver
      jdbc.url=jdbc:hsqldb:mem:my-project-test;shutdown=true
      jdbc.username=sa
      jdbc.password=
      
    6. 於res下加入mybatis-config.xml
      
        
        
          
            
            
              
              
              
              
            
          
        
        
          
        
      
      

      這是主要讓mybatis設定環境連線參數的設定檔
    7. 於res下加入UserMapper.xml
      
        < select id="getUserById" parametertype="String" resulttype="cmpnts.User">
           SELECT 
            userid as userid, 
            email as email , 
            password, 
            name as Name
           FROM USER 
           WHERE USERID = #{userId}
        </ select>
      
      

      (以上select開頭的tag空格請自行刪除,因為不用空格會被Blogger判定為HTML select)
      因為這篇只做SELECT因此以上設定檔只列出SELECT,可以看到SQL的語法直接寫在Mapper設定中就好了!非常的方便! 甚至若有邏輯上判斷的需求也可以直接寫在這,詳細請看這Dynamic SQL
    8. 建立package cmpnts,並加入User Class
      package cmpnts;
      
      public class User {
       private String userid;
       private String name;
       private String password;
       private String email;
      
       public String getUserId() {
        return userid;
       }
      
       public void setUserId(String userid) {
        this.userid = userid;
       }
      
       public String getName() {
        return name;
       }
      
       public void setName(String username) {
        this.name = username;
       }
      
       public String getPassword() {
        return password;
       }
      
       public void setPassword(String pwd) {
        this.password = pwd;
       }
       
       public String getEmaill(){
        return email;
       }
       
       @Override
       public String toString(){
        StringBuffer buf = new StringBuffer();
        buf.append("UserId:").append(this.getUserId());
        buf.append("\r\n\tname:").append(this.getName());
        buf.append("\r\n\tpassword:").append(this.getPassword());
        buf.append("\r\n\temail:").append(this.getEmaill());
        return buf.toString();
       }
      }
      
    9. 加入UserMapper Class

      package cmpnts;
      import java.util.List;
      
      public interface UserMapper{
       //public void insertUser(User user);
       public User getUserById(String userId);
       //public List getAllUsers();
       //public void updateUser(User user);
       //public void deleteUser(Integer userId);
      }
      

      這是我們的UserMapper Interface,由於這篇我們只會使用SELECT,因此實際上使用到的只有getUserById(String userId);


      到這邊準備工作都完成了,可以開始呼叫測試看結果了。
    10. 呼叫方式如下:
      public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        InputStream inputStream;
        try {
           inputStream = Resources.getResourceAsStream(resource);
           SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
           .build(inputStream);
      
           createTableUser();
                      
           User user = UserDAO.getUserById(sqlSessionFactory,"Q011");
           System.out.println(user);
      
        } catch (Exception e) {
           e.printStackTrace();
        }
      }
      

      其中的createTableUser()我使用HSQLDB來建立Table,由於跟MyBatis並無關聯因此就不列出了, 習慣使用其他DataBase的直接依照一般習慣建立Table使用即可。(記得上面的jdbc.properties也要更改為自行使用的DataBase設定)
    11. UserDAO.java
      public static User getUserById(SqlSessionFactory sqlSessionFactory, String userid) {
         SqlSession sqlSession = sqlSessionFactory.openSession();
         try {
       UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
       return userMapper.getUserById(userid);
         } finally {
       sqlSession.close();
         }
      }
      

      • 首先我們傳入在main中建立的SqlSessionFactory
      • 使用SqlSessionFactory建立SqlSession 
      • 利用SqlSession獲得UserMapper的對應
      • 呼叫 getUserById(),取得我們要的結果User
      • 關於各種mybatis物件的life time請參考Getting started下面的Scope and Lifecycle
    12. 以下就是main呼叫後取得的User Println()結果
      UserId:Q011
       name:Q011anne
       password:annepwd123
       email:anne@mail.piwi.com.tw
      

    以上看起來很神奇,其實說穿了就是使用Java Reflection的機制來做的(請見Java Reflection - 存取Field 資訊);
    實際上測試了一下,他是直接對應field name;也就是說在User.java中的各種field name: userid、name、password、email都要能與在UserMapper.xml中寫下的SELECT Query相符(大小寫不限):
    SELECT 
          userid as Userid, 
          email as Email , 
          password, 
          name as Name
         FROM USER 
         WHERE USERID = #{userId}

    而與method name(getter、setter)沒有關聯。

    2013/9/7補充:
       上述的SELECT語法,若沒有使用ResultMap一次傳回大量資料(請見此篇MyBatis - 使用resultMap、resultType自行定義回傳資料)則務必要使用如上述的 userid as userid 這種方式才有辦法對應的到,後面的 as userid,一定要使用回傳Java Type的field name,比如如果習慣field name使用_usierid、_eamil,就一定要寫成
    SELECT 
          userid as _userid, 
          email as _email, 
          password as _password, 
          name as _name
         FROM USER 
         WHERE USERID = #{userId}
    否則怎麼回傳都會是Null Data,倒是 #{userId}使用甚麼名稱都無所謂,使用 #{xx}也是可以的,應該是只要param順序對應到就可以

    但是,如果像我Class field都習慣前面加上 _name、_userid這種方式,DB的field又不能修改成這樣,該怎麼辦呢? 這時候可以藉助好用的resultMap機制達到喔,請見此篇MyBatis - 使用resultMap、resultType自行定義回傳資料

    2013年8月21日 星期三

    svn - 允許commit之後修改log

    參考:Subversion 不能修改紀錄訊息



    1. 切換到想要的repository/hooks
    2. 使用mv pre-revprop-change.tmpl pre-revprop-change
    3. chmod +x pre-revprop-change (將檔案加入執行權限)
    4. vi pre-revprop-change,修改後內容如下
      REPOS="$1"
      REV="$2"
      USER="$3"
      PROPNAME="$4"
      ACTION="$5"

      if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi

      echo "Changing revision properties other than svn:log is prohibited" >&2
      exit 1
    5. 存檔離開,就可以edit log了


    2013年8月16日 星期五

    Tomcat - 更改預設首頁

    需求:
    一般預設首頁都為index.html、index.jsp...我們需要增加預設首頁為index.shtml


    1. 找到對應的WEB-INF\web.xml
    2. 找到下面這一段
        
          index.shtml
          index.html
          index.htm
          index.jsp
          default.html
          default.htm
          default.jsp
        
      

      如上所示,加入想要的預設首頁完整名稱
    3. 存檔、重新開啟Tomcat

    c (windows)- 將char、wchar、tchar轉換為unsigned long、unsigned long long

    參考:MSDN - strtoul, _strtoul_l, wcstoul, _wcstoul_l  、  strtoull-replacement 、C语言的三种整型数据类型:int、short int和long int
    需求:

    1. 將char、wchar、tchar轉換為unsigned long (下面以TCHAR為範例):
        unsigned long len = 0;
        TCHAR queryBuf[32]  = _T("6588775699");
        len = _tcstoul(queryBuf,NULL,0);
      

      其餘對應為:strtoul, _strtoul_l, wcstoul, _wcstoul_l
    2. 將char、wchar、tchar轉換為unsigned long longg (下面以TCHAR為範例):
        unsigned long long len = 0;
        TCHAR queryBuf[32]  = _T("6588775699");
        len = _tcstoui64(queryBuf,NULL,0);
      

      其餘對應為:_strtoui64(), _wcstoui64() and _tcstoui64()
    3. 而使用printf、sprintf的unsigned long long的對應format為%llu:
                      
         unsigned long long ll =  6588775699;
         TCHAR tszHeader[MAX_FILENAME_LEN] = {0};
         _stprintf(tszHeader,_T("Range:bytes=%llu-"),ll);
      

    2013年8月15日 星期四

    android - 使用ListView(2) 定義Item按下去後的行為

    需求:
    承襲上一篇【使用ListView(1)】,現在我們需要定義當按下ListView中的Item後的事件行為:
    Item按下去後以Toast顯示物品名稱


    1. 建立 ItemClick
      此Class負責當Item按下去後需產生的行為,extend OnItemClickListener
      public class ItemClick implements OnItemClickListener {
       private Context    _context;
       private List< mydatainfo > _infoList;
       
       @SuppressWarnings("unused")
       private ItemClick(){
       }
       
       public ItemClick(Context context,List< mydatainfo > list){
        _context  = context;
        _infoList = list;
       }
      
       @Override
       public void onItemClick(AdapterView arg0, View arg1, int position, long arg3) {
        String text = _infoList.get(position).getName();
        Toast.makeText(_context, "按下"+text, Toast.LENGTH_SHORT).show();
       }
      }
      

    2. 將 ItemClick 設定至 ListView
      我們使用上一篇文章的Code,加入此行(將原先的code反註解即可):
      listview.setOnItemClickListener(new ItemClick(this, _datas));
      

      原先的onCreate()變更為如下:
       @Override
       protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);
        
        _datas = new ArrayList< mydatainfo >();
        _listviewDataAdapter = new MyDataAdapter(this,_datas);
        
        addNewData(R.drawable.mycard_50_app,"MyCard點數50",50);
        addNewData(R.drawable.mycard_100_app,"MyCard點數100",100);
        
        ListView listview = (ListView) findViewById(R.id.listView1);
        listview.setAdapter(_listviewDataAdapter);
        listview.setOnItemClickListener(new ItemClick(this, _datas));
       }
       
       private void addNewData(int resourceid,String name,int price){
        Drawable  icon = getResources().getDrawable(resourceid);
        _datas.add(new MyDataInfo(name,price,icon)); 
       }
      

    2013年8月13日 星期二

    log4j - 如何設定log檔案依照日期來區分檔案名稱

    需求:
    希望log的紀錄依照日期的不同,每一個日期獨立一個檔案,類似
    serverlog-2013-08-11.log、serverlog-2013-08-12.log ... etc


    1. 寫一個專門處理的Logger class(可隨意,但我習慣這樣使用)
      package utilities;
      
      import java.text.SimpleDateFormat;
      import java.util.Date;
      
      import org.apache.log4j.Logger;
      import org.apache.log4j.PropertyConfigurator;
      
      public class Logger4j {
      
       static{
        initLoggerDateParam();
       }
      
       private static void initLoggerDateParam(){
           SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
           System.setProperty("log4j.date", dateFormat.format(new Date()));
       }
       private static Logger initLogger(){
        PropertyConfigurator.configure("infoerror.properties");
        Logger logger = Logger.getLogger(Logger4j.class);
        
        initLoggerDateParam();
        return logger;
       }
       
       public static void logInfo(String msg){
        initLogger().info(msg);
       }
       
       public static void logError(Exception ex){
        initLogger().debug(ex.getMessage(), ex.getCause());
       }
      }
        

      重點為其中的initLoggerDateParam()部分,在這裡我們設定一個系統變數log4j.date,為了提供給log4j設定檔使用
    2. 撰寫log4j設定檔infoerror.properties

    3. log4j.rootLogger=Info, A1, A2
      
      
      # A1 is set to be a ConsoleAppender
      log4j.appender.A1=org.apache.log4j.ConsoleAppender
      log4j.appender.A1.layout=org.apache.log4j.PatternLayout
      log4j.appender.A1.layout.ConversionPattern=[%d{yy/MM/dd HH:mm:ss}][%p][%C-%L] %m%n
      
      # A2 is set to be a file
      log4j.appender.A2=org.apache.log4j.DailyRollingFileAppender
      log4j.appender.A2.layout=org.apache.log4j.PatternLayout
      log4j.appender.A2.layout.ConversionPattern=[%d{yy/MM/dd HH:mm:ss}][%p][%C-%L] %m%n
      log4j.appender.A2.File=./log/gpserver-${log4j.date}.log
      
      

      重點為log4j.appender.A2.File=./log/appname-${log4j.date}.log 我們使用${log4j.date}提取出java中設定的系統變數log4j.date
    4. 重新啟動程式,log檔案就會在相對應的位置囉

    2013年8月8日 星期四

    tomcat - 支援中文字網址



    1. 更改conf\server.xml
    2. 找到<Connector port="80" ...的設定
    3. 加上 URIEncoding="UTF-8" 的屬性 <Connector port="8080" URIEncoding="UTF-8"  />

    android - tabview 無法顯示出 ICON的問題

    這兩天來練習使用TabView,由於我的目標是HoneyComb以下的版本也能使用,因此我採用的是在HoneyComb後已經被deprecated的作法,我參考此教學實作,遇到了個問題:

    就是我的Tab Icon無法正常顯示!!
    原先欲產生效果圖應該如下:
    但最後產出的變成:
    找了半天都搞不清楚為什麼Icon無法正常顯示!後來才發現是manifest的問題。我的AndroidManifest有這樣的定義:

        
    

    關鍵點就是這個android:targetSdkVersion="17"屬性,測試後發現數值在10(包含)以下,Icon都能正常顯示, 超過後就無法正常顯示,推測應該是因為11以後代表的就是HoneyComb版本,而這種使用TabView(extend Activity)的方式在HoneyComb為deprecated ,因此無法正常顯示。
    其實拿掉這個屬性也是沒有問題的

    Android_version 對應參考
    android新版本TabActivity作法

    2013年8月7日 星期三

    android - 檢查網路連線狀態

    這個功能應該算挺常見的,直接看Code:


    import android.content.Context;
    import android.net.ConnectivityManager;
    import android.net.NetworkInfo;
    
    public class SystemUtils {
    
     public static boolean checkNetworkStatus(Context context){
      ConnectivityManager conMgr =  (ConnectivityManager)context.getSystemService(context.CONNECTIVITY_SERVICE);
    
      if(conMgr.getActiveNetworkInfo() == null)
     return false;
      
      if(conMgr.getActiveNetworkInfo().getState() == NetworkInfo.State.CONNECTED  )
     return true;
      
      return false;
     }
    }
    
    

    之後記得在Manifest.xml加入permission:
    
    

    2013年8月5日 星期一

    android - 得知使用者按下Back Button並發出確認警告?

    需求: 有一個下載檔案的AsyncTask,若使用者按下Back Button時需要能退出並結束下載的Task: 直接看Code:
    1.在MainActivity上加入Back Button的監聽實做:
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         if(keyCode == KeyEvent.KEYCODE_BACK) {
       DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
           @Override
           public void onClick(DialogInterface dialog, int which) {
               switch (which){
               case DialogInterface.BUTTON_POSITIVE:
                finish();
                   break;
               case DialogInterface.BUTTON_NEGATIVE:
                   break;
               }
           }
       };
       AlertDialog.Builder builder = new AlertDialog.Builder(this);
       builder.setMessage("退出後現有下載的檔案進度將會停止,確定退出?").setPositiveButton("退出", dialogClickListener)
           .setNegativeButton("否", dialogClickListener).show();
       return true;
         }
         return super.onKeyDown(keyCode, event);
     }
    

    2.在下載檔案的MainActivity onDestroy()中加入:
     @Override
     protected void onDestroy(){
      super.onDestroy();
      DownloadFile.getNowDownloadTask().cancel(true);
     }
    

    2013年8月2日 星期五

    android exception - 出現java.net.UnknownHostException: Unable to resolve host

    使用

    URL url = new URL(sUrl[0]);
    URLConnection connection = url.openConnection();
    connection.connect();

    時出現java.net.UnknownHostException: Unable to resolve host

    加上permission:
    <uses-permission android:name="android.permission.INTERNET" />