整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Hessian 序列化、反序列化

          Hessian 序列化、反序列化

          問題和思考:

          1. 序列化參數有枚舉屬性,序列化端增加一個枚舉,能否正常反序列化?
          2. 序列化子類,它和父類有同名參數,反序列化時,同名參數能否能正常賦值?
          3. 序列化對象增加參數,反序列化類不增加參數,能否正常反序列化?
          4. 用于序列化傳輸的屬性,用包裝器比較好,還是基本類型比較好?

          為什么要使用序列化和反序列化

          1.程序在運行過程中,產生的數據,不能一直保存在內存中,需要暫時或永久存儲到介質(如磁盤、數據庫、文件)里進行保存,也可能通過網絡發送給協作者。程序獲取原數據,需要從介質,或網絡傳輸獲得。傳輸的過程中,只能使用二進制流進行傳輸。

          2.簡單的場景,基本類型數據傳輸。通過雙方約定好參數類型,數據接收方按照既定規則對二進制流進行反序列化。

          3.復雜的場景,傳輸數據的參數類型可能包括:基本類型、包裝器類型、自定義類、枚舉、時間類型、字符串、容器等。很難簡單通過約定來反序列化二進制流。需要一個通用的協議,共雙方使用,進行序列化和反序列化。

          三種序列化協議及對比

          序列化協議

          特點

          jdk
          (jdk 自帶)

          1. 序列化:除了 static、transient類型
          2. 特點:強類型,安全性高,序列化結果攜帶類型信息
          3. 反序列化:基于 Field 機制
          4. 應用場景:深拷貝

          fastjson
          (第三方實現)

          1. 可讀性好,空間占用小
          2. 特點:弱類型,序列化結果不攜帶類型信息,可讀性強。有一些安全性問題
          3. 反序列化:基于 Field 機制,兼容 Bean 機制
          4. 應用場景:消息、透傳對象

          hessian
          (第三方實現)

          1. 序列化:除了 static、transient 類型
          2. 特點:強類型,體積小,可跨語言,序列化結果攜帶類型信息
          3. 反序列化:基于 Field 機制,兼容 Bean 機制
          4. 應用場景:RPC

          對比

          Father father=new Father();
          father.name="廚師";
          father.comment="川菜館";
          father.simpleInt=1;
          father.boxInt=new Integer(10);
          father.simpleDouble=1;
          father.boxDouble=new Double(10);
          father.bigDecimal=new BigDecimal(11.5);
          

          運行結果:

          jdk序列化結果長度:626,耗時:55
          jdk反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:87
          
          hessian序列化結果長度:182,耗時:56
          hessian反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:7
          
          Fastjson序列化結果長度:119,耗時:225
          Fastjson反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:69
          

          分析:

          • jdk 序列化耗時最短,但是序列化結果長度最長,是其它兩種的 3 ~ 5 倍。
          • fastjson 序列化結果長度最短,但是耗時是其它兩種的 4 倍左右。
          • hessian 序列化耗時與 jdk 差別不大,遠小于 fastjson 序列化耗時。且與 jdk 相比,序列化結果占用空間非常有優勢。另外,hessian 的反序列化速度最快,耗時是其它兩種的 1/10。
          • 綜上比較,hessian 在序列化和反序列化表現中,性能最優。

          Hessian 序列化實戰

          實驗準備

          父類

          public class Father implements Serializable {
          
              /**
               * 靜態類型不會被序列化
               */
              private static final long serialVersionUID=1L;
          
              /**
               * transient 不會被序列化
               */
              transient int version=0;
          
              /**
               * 名稱
               */
              public String name;
          
              /**
               * 備注
               */
              public String comment;
          
              /**
               * 包裝器類型1
               */
              public Integer boxInt;
          
              /**
               * 基本類型1
               */
              public int simpleInt;
          
              /**
               * 包裝器類型2
               */
              public Double boxDouble;
          
              /**
               * 基本類型2
               */
              public double simpleDouble;
          
              /**
               * BigDecimal
               */
              public BigDecimal bigDecimal;
          
              public Father() {
              }
          
              @Override
              public String toString() {
                  return "Father{" +
                          "version=" + version +
                          ", name='" + name + '\'' +
                          ", comment='" + comment + '\'' +
                          ", boxInt=" + boxInt +
                          ", simpleInt=" + simpleInt +
                          ", boxDouble=" + boxDouble +
                          ", simpleDouble=" + simpleDouble +
                          ", bigDecimal=" + bigDecimal +
                          '}';
              }
          }
          

          子類

          public class Son extends Father {
          
              /**
               * 名稱,與father同名屬性
               */
              public String name;
          
              /**
               * 自定義類
               */
              public Attributes attributes;
          
              /**
               * 枚舉
               */
              public Color color;
          
              public Son() {
              }
          
          }
          

          屬性-自定義類

          public class Attributes implements Serializable {
          
              private static final long serialVersionUID=1L;
          
              public int value;
          
              public String msg;
          
              public Attributes() {
              }
          
              public Attributes(int value, String msg) {
                  this.value=value;
                  this.msg=msg;
              }
          
          }
          

          枚舉

          public enum Color {
          
              RED(1, "red"),
              YELLOW(2, "yellow")
              ;
          
              public int value;
          
              public String msg;
          
              Color() {
              }
          
              Color(int value, String msg) {
                  this.value=value;
                  this.msg=msg;
              }
          }
          

          使用到的對象及屬性設置

          Son son=new Son();
          son.name="廚師";    // 父子類同名字段,只給子類屬性賦值
          son.comment="川菜館";
          son.simpleInt=1;
          son.boxInt=new Integer(10);
          son.simpleDouble=1;
          son.boxDouble=new Double(10);
          son.bigDecimal=new BigDecimal(11.5);
          son.color=Color.RED;
          son.attributes=new Attributes(11, "hello");
          

          運行結果分析

          使用 Hessian 序列化,結果寫入文件,使用 vim 打開。使用 16 進制方式查看,查看命令:%!xxd

          00000000: 4307 6474 6f2e 536f 6e9a 046e 616d 6504  C.dto.Son..name.
          00000010: 6e61 6d65 0763 6f6d 6d65 6e74 0662 6f78  name.comment.box
          00000020: 496e 7409 7369 6d70 6c65 496e 7409 626f  Int.simpleInt.bo
          00000030: 7844 6f75 626c 650c 7369 6d70 6c65 446f  xDouble.simpleDo
          00000040: 7562 6c65 0a61 7474 7269 6275 7465 7305  uble.attributes.
          00000050: 636f 6c6f 720a 6269 6744 6563 696d 616c  color.bigDecimal
          00000060: 6002 e58e a8e5 b888 4e03 e5b7 9de8 8f9c  `.......N.......
          00000070: e9a6 869a 915d 0a5c 430e 6474 6f2e 4174  .....].\C.dto.At
          00000080: 7472 6962 7574 6573 9205 7661 6c75 6503  tributes..value.
          00000090: 6d73 6761 9b05 6865 6c6c 6f43 0964 746f  msga..helloC.dto
          000000a0: 2e43 6f6c 6f72 9104 6e61 6d65 6203 5245  .Color..nameb.RE
          000000b0: 4443 146a 6176 612e 6d61 7468 2e42 6967  DC.java.math.Big
          000000c0: 4465 6369 6d61 6c91 0576 616c 7565 6304  Decimal..valuec.
          000000d0: 3131 2e35 0a                             11.5.
          

          對其中的十六進制數逐個分析,可以拆解為一下結構:參考 hessian 官方文檔,鏈接:http://hessian.caucho.com/doc/hessian-serialization.html

          序列化原理

          序列化規則:

          1.被序列化的類必須實現了 Serializable 接口

          2.靜態屬性和 transient 變量,不會被序列化。

          3.枚舉類型在序列化后,存儲的是枚舉變量的名字

          4.序列化結果的結構:類定義開始標識 C -> 類名長度+類名 -> 屬性數量 -> (逐個)屬性名長度+屬性名 -> 開始實例化標識 -> (按照屬性名順序,逐個設置)屬性值(發現某個屬性是一個對象,循環這個過程)

          反序列化

          通俗原理圖:

          解釋:這是前邊的序列化文件,可以對著這個結構理解反序列化的過程。

          解釋:讀取到“C”之后,它就知道接下來是一個類的定義,接著就開始讀取類名,屬性個數和每個屬性的名稱。并把這個類的定義緩存到一個_classDefs 的 list 里。

          解釋:通過讀取序列化文件,獲得類名后,會加載這個類,并生成這個類的反序列化器。這里會生成一個_fieldMap,key 為反序列化端這個類所有屬性的名稱,value 為屬性對應的反序列化器。

          解釋:讀到 6 打頭的 2 位十六進制數時,開始類的實例化和賦值。

          遺留問題解答:

          • 增加枚舉類型,反序列化不能正常讀取。

            • 原因:枚舉類型序列化結果中,枚舉屬性對應的值是枚舉名。反序列化時,通過枚舉類類名+枚舉名反射生成枚舉對象。枚舉名找不到就會報錯。
          • 反序列化為子類型,同名屬性值無法正常賦值。

          • 序列化對象增加參數,反序列化可以正常運行。

            • 原因:反序列化時,是先通過類名加載同名類,并生成同名類的反序列化器,同名類每個屬性對應的反序列化器存儲在一個 map 中。在反序列化二進制文件時,通過讀取到的屬性名,到 map 中獲取對應的反序列化器。若獲取不到,默認是 NullFieldDeserializer.DESER。待到讀值的時候,僅讀值,不作 set 操作
          • 序列化和反序列化雙方都使用對象類型時,更改屬性類型,若序列化方不傳輸數據,序列化結果是‘N’,能正常反序列化。但是對于一方是基本類型,更改屬性類型后,因為 hessian 對于基本類型使用不同范圍的值域,所以無法正常序列化。


          參考文檔:

          • https://zhuanlan.zhihu.com/p/44787200
          • https://paper.seebug.org/1131/
          • hessian 官方文檔:序列化規則http://hessian.caucho.com/doc/hessian-serialization.html#anchor10
          • ASCII 編碼對照表http://ascii.911cha.com/

          信大多數的Java后端開發者都聽說過MyBatis,但是用過JPA的人卻相對較少,有些人可能沒有聽說過。JPA在國外比較火用的人也比較多,國內大多被MyBatis的簡單易上手所吸引。相對的,JPAMyBatis二者之間并沒有可比性,它們所在的層次不一樣,MyBatisHibernate一樣屬于ORM層級,而JPA則是在ORM之上Spring做了一些高度的封裝,使之更便于使用。很多人覺得它的學習成本比較高,其實不然。接下來盤點下JPA中的一些使用方法。

          JPA官方文檔:https://docs.spring.io/spring-data/jpa/docs/2.5.3/reference/html

          依賴引入

          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-data-jpa</artifactId>
          </dependency>
          

          使用SQL進行CRUD

          編寫SQL語句是最基礎的操作,它支持JPQL、HQL(語法無關數據庫類型,底層會自動生成對應數據庫的SQL如MySQL)和原生SQL(需要開發者保證語法與當前連接的數據庫一致)。

          示例代碼,以下使用了JPQL的語法

          @Repository
          public interface UserDao extends JpaRepository<User, Long> {
              //獲取某條數據
              @Query(value="select user from User r where r.username=:username")
              User findFirstByUsername(@Param("username") String username);
          
              //也可以獲取某一個字段,而不是整行數據
              @Query(value="select user.username from User r where r.id=:id")
              String getUsernameById(@Param("id") Long id);
          
              //刪除一條數據,注意這里不寫SQL語句也是可以的,語義判定,后面會講
              @Modifying
              void deleteUserById(Long id)
          }
          

          保存和更新則更加簡單,直接調用save方法,傳入實體類實例。

          public class UserServiceImpl implements UserService {
              @Autowired
              private UserDao userDao;
          
              @Override
              @Transactional(rollbackFor=Throwable.class)
              public void createUser(User user) {
                  //創建和更新都是使用的save
                  userDao.save(user);
              }
          }
          

          以上就是最基礎最簡單的用法。

          如何執行數據庫原生SQL?

          上面的章節都是使用了數據庫無關的JPQL語法格式,如果想執行某數據庫特有的SQL函數時,可以指定使用原生SQL,如下代碼引用了官方文檔:

          public interface UserRepository extends JpaRepository<User, Long> {
          
            @Query(value="SELECT * FROM USERS WHERE LASTNAME=?1",
              countQuery="SELECT count(*) FROM USERS WHERE LASTNAME=?1",
              nativeQuery=true)
            Page<User> findByLastname(String lastname, Pageable pageable);
          }
          

          以上代碼中使用nativeQuery=true指定了使用原生的SQL,值得注意的是,如果后期更換了數據庫服務,比如從MySQL改為了Oracle,這塊代碼就極有可能不兼容。 Pageable pageable是分頁信息。

          無需寫SQL,根據方法語義自動生成實現方法

          先看下如下代碼

          public interface UserRepository extends Repository<User, Long> {
            List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
          }
          

          可以看出,我沒有寫任何SQL,也不需要實現該接口,就可以直接使用該接口進行查詢。這就是JPA的智能語義分析機制,它在源碼中的實現原理是運行時生成一個代理類,注入到依賴的bean中,根據方法的語義生成實際執行的SQL語句。

          如上述代碼中方法名findByEmailAddressAndLastname中的findBy便是一個查詢的主題關鍵字,EmailAddressLastname是User表中的字段,And則是判斷的關鍵字。于是,該方法名的含義是根據email地址和lastname查詢用戶列表。那么在生成代理方法時就會根據這些關鍵字生成SQL語句。

          常用的主題關鍵字有findBy、getBy、deleteXxxBy、countXxxBy等,條件判斷的關鍵字有And、Or、In等。

          更多的關鍵字及用法可以查閱官方文檔:https://docs.spring.io/spring-data/jpa/docs/2.5.3/reference/html/#appendix.query.method.predicate

          如何只查詢部分字段并自動填充返回Pojo/DTO實例?

          查詢整行數據是很有可能造成回表查詢的,除非是聚簇索引下使用主鍵查詢,因此大多數情況下是只查詢業務關心的字段,而不是整行數據。

          通常情況下,一個表對應了一個實體類

          以下是一個實體類的定義示例:

          @javax.persistence.Table(name="test_user", uniqueConstraints={@UniqueConstraint(columnNames={"username", "emailAddress"})})
          @javax.persistence.Entity
          public class User implements Serializable {
              private Long id;
              private String role;
              private String username;
              private String emailAddress;
              @Id
              @GenericGenerator(name="snowFlakeIdGenerator", strategy="自定義的id生成策略類")
              @GeneratedValue(generator="snowFlakeIdGenerator")
              @JsonFormat(shape=JsonFormat.Shape.STRING) //Java中的long比js中的長,使用String傳輸
              public Long getId() {
                  return id;
              }
              public void setId(Long id) {
                  this.id=id;
              }
              public String getRole() {
                  return role;
              }
              public void setRole(String role) {
                  this.role=role;
              }
              public String getUsername() {
                  return username;
              }
              public void setUsername(String username) {
                  this.username=username;
              }
          }
          

          接下來定義一個對外接口中用于傳輸的DTO類

          package com.demo.dto;
          
          public class UserInfoDTO {
              private Long id;
              private String username;
              private String emailAddress;
              //定義一個有參的構造函數,用于在JPQL語句中構建DTO的實例
              public UserInfoDTO(Long id, String username, String emailAddress) {
                  this.id=id;
                  this.username=username;
                  this.emailAddress=emailAddress;
              }
              public UserInfoDTO() {
                  
              }
              // 以下Set、Get方法省略
          }
          

          查詢部分字段,并將字段構造成Pojo/DTO對象

          public interface UserRepository extends Repository<User, Long> {
              //注意這里返回的是UserInfoDTO而不是User,因此需要編寫JPQL語句
              @Query(value="select new com.demo.dto.UserInfoDTO(r.id, r.username, r.emailAddress) from User r where r.username=:username")
              UserInfoDTO getUserInfoByName(String username);
          }
          

          代碼中select new com.demo.dto.UserInfoDTO(r.id, r.username, r.emailAddress)是將查到的id、username、emailAddress字段作為構造參數傳入構建UserInfoDTO實例,沒有進行整表查詢。

          其返回的UserInfoDTO實例就是最終需要的,不需要先查出來User在進行復制。

          多表連接查詢

          在JPA中也可以直接使用JPQL進行多表連接查詢。上一章節講到了可以將查詢到的結果返回自定義的類,只需要重載一個有參的構造函數。與普通的SQL無異,在JPQL的語句中也可以使用left join等關鍵字,將需要的字段信息傳入構造函數中即可。

          大多數開發者應該都使用過Hibernate或者Mybatis的框架,或多或少都踩過一些坑!如在MyBatis/Ibatis中#和$的區別,#方式能夠很大程度防止sql注入,$方式無法防止Sql注入。所以,老司機 對新手說,最好用#。簡單的說#{}是經過預編譯的,是安全的,而${}是未經過預編譯的,僅僅是取變量的值,是非安全的,存在sql諸如,有些特例是需要關注的,有的時候需要用¥{}解決一些實際問題。如在執行sql語句時你有時并不希望讓變量進行處理,而是直接賦值執行,這時就要用到(${a})了,在使用時還要這樣賦值 @Param(value="a") String a

          如日期問題:

          可能會遇到日期格式的時間段問題,當數據庫的時間為DATE類型時,MyBatis的jdbcType應該使用DATE

          jdbcType=DATE,而不是使用jdbcType=TIMESTAMP

          如在使用resultMap的時候,要把ID寫在第一行,否則的話,就會報錯。

          又如最近在做的項目,遇到myBatis的大坑,Mybatis一直報異常Java.lang.ArrayIndexOutOfBoundsException,于是開始代碼查錯,代碼中有存儲過程,然后開發使用ROOT用戶執行SQL跑出來的數據結果集是正常的,在測試環境程序運行也正常,但是在正式環境就其他用戶不行,最后發現是因為數據庫沒有給該用戶授權出了問題。


          案例一:

          作為新手,在此記下剛踩的一個坑,(踩踩更健康==踩過痛過才不會再次錯),寫了一個sql語句用到兩張表,兩張表中有兩個字段名字是一樣的都是Time和Content,然后要查詢這兩張表的這兩個字段都要查出來放到一個dto中,dto如下圖所示,



          sql語句如下,



          然而運行后卻發現后幾個在數據庫表里同名的字段取出來都是null,



          但是放到數據庫那邊執行是沒有取出空數據的,



          真是苦惱==,后來經大神指點,sql語句查詢出來的這個字段名必須和dto的參數名一致,改成這樣就通過了,



          數據都取出來了。。。。。。。。。。還記得在hibernate里用hql時放到dto里,select new dto名()參數順序和類型一致就可以取出來。。。這應該算一個不同點吧,,感覺還是hql用起來舒服,,,求大神科普兩者的差別優缺點

          當實體類中的屬性名和表中的字段名不一致時,使用MyBatis進行查詢操作時無法查詢出相應的結果的問題,當時上網查了很多才知道,看到的一個解決方法分享給大家,通過來映射字段名和實體類屬性名的一一對應關系。這種方式是使用MyBatis提供的解決方式來解決字段名和屬性名的映射關系的!

          案例二:

          數據庫表使用了聯合主鍵,逆向生成的時候生成了兩個實體類。看起來別扭。但還是可以用。后來就先取消主鍵,生成完后再將主鍵加上。還有就是,tinyint本來以為用來表示比較小的整數,結果生成了布爾型的屬性。后來就表示是和否才用tinyint了。逆向生成的sql語句絕對不能人為改動,否則再次生成的時候會重復生成。但是,盡管踩過坑,我還是覺得mybatis超級好用,比hibernate好多了。雖然hibernate我只試過一點之后就完全轉向了mybatis了。

          案例三:

          sum()和count()使用場景不對導致出錯:

          count()、count(1)、count(0)就是指絕對的行數,哪怕某行所有字段全部為null也會計算在內。count(1)和count()相比,innodb來說count(*)效率低。

          如果count(列名)查詢出來的結果就是查出列名中不為null的行數;

          sum(列名)對指定列名進行求和

          MyBatis把int類型的0處理成空串’’和mysql處理空串’’為0的問題,在Mybatis的Mapper中整數類型條件該如何判斷?

          當數據庫字段類型是整數,如果參數變量為空字符串或者NULL,Mybatis會自動將參數賦值0,所以如果要判斷整數參數的多種狀態在傳遞數值到Mapper之前就要判斷是否為空字符串和NULL并將相應的狀態數值賦值給該參數,否則參數值等于空字符串、NULL和0得到的結果是一樣的。

          一般情況下,涉及到int類型的操作的時候,在Service中會統一把數字類型先變成字符串類型,然后再傳遞到Mapper中操作。

          時間戳的使用

          在創建新記錄的時候把這個字段設置為當前時間,但以后修改時,不再刷新它(可以給createtime使用這個):TIMESTAMP DEFAULT CURRENT_TIMESTAMP

          在創建新記錄和修改現有記錄的時候都對這個數據列刷新(可以給update使用這個):

          TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

          在使用resultMap的時候,要把ID寫在第一行,否則的話,就會報錯。


          案例四:

          XML轉義字符,如果直接寫就會報錯,需要用左邊一列的轉義字符

          < < 小于號 > > 大于號

          & & 和

          ' ' 單引號

          " " 雙引號

          案例五:

          前幾天在項目中碰到,來說下吧。大神可繞道。在使用selectOne查詢個數時,

          如果你寫了resultType為Integer,然后在業務代碼中很自然的用一個變量int去接當前這個方法的返回,如果按照你傳入的條件在數據庫中沒有找到相關的值,此時selectOne方法的返回值會是一個null,當你使用Java的自動拆箱機制的時候會報出一個無情的NPE。

          原因:Java在自動拆箱的時候會調用Integer類中的intValue方法,如果當前對象為null,則拋出NPE。

          因此,在接受的時候要判空,否則可能異常。

          案例六:

          多參數的使用

          MyBatis的查詢或者更新中,如果需要多個參數有如下幾種辦法:

          對象映射,建立一個Java對象,并作為接口的參數,對象的屬性可以直接使用#{屬性名}的方式訪問;

          Map, 參數為一個Map, key對于屬性名,value對于參數值,這個方法就是傳參數是需要建立一個Map的臨時對象

          @param參數注解,在接口方法參數前加入參數名稱注解,這樣就可以直接在Mapper中通過參數名訪問

          通過序號訪問,第一個參數#{0}或#{param1}, 第二個參數#{1}, #{param2}

          MyBatis中時間字段的使用–返回

          時間字段的返回目前筆者采用放回字符串的方式:

          date_format(update_time, ‘%Y-%c-%d %H:%i:%s’) updatetime

          采用MySQL的時間格式化方法。

          或者放回Timestamp類型的數據,要求放回對象屬性參數為Timestamp.

          MyBatis中時間字段的使用–參數

          如果需要查詢一段時間范圍的數據時,可以通過以下動態SQL的方式查詢數據:

          and lbr.update_time > #{startTime}

          and lbr.update_time < #{endTime, javaType=Date, jdbcType=TIMESTAMP}

          對于的接口方法名稱如下:

          … Date startTime, Date endTime…

          我想這個方法會比通過格式轉換的效率要高一些

          1. MyBatis中時間字段的使用–寫入
          2. 寫入可是直接寫入Timestamp的數據,需要描述一些寫入的jdbcType,如下:

          {installTime, jdbcType=TIMESTAMP}

          1.Mapper層參數為Map,由Service層負責重載。

          Mapper由于機制的問題,不能重載,參數一般設置成Map,但這樣會使參數變得模糊,如果想要使代碼變得清晰,可以通過service層來實現重載的目的,對外提供的Service層是重載的,但這些重載的Service方法其實是調同一個Mapper,只不過相應的參數并不一致。

          也許有人會想,為什么不在Service層也設置成Map呢?我個人是不推薦這么做的,雖然為了方便,我在之前的項目中也大量采用了這種方式,但很明顯會給日后的維護工作帶來麻煩。因為這么做會使你整個MVC都依賴于Map模型,這個模型其實是很不錯的,方便搭框架,但存在一個問題:僅僅看方法簽名,你不清楚Map中所擁有的參數個數、類型、每個參數代表的含義。

          試想,你只對Service層變更,或者DAO層變更,你需要清楚整個流程中Map傳遞過來的參數,除非你注釋或者文檔良好,否則必須把每一層的代碼都了解清楚,你才知道傳遞了哪些參數。針對于簡單MVC,那倒也還好,但如果層次復雜之后,代碼會變得異常復雜,而且如果我增加一個參數,需要把每一個層的注釋都添加上。相對于注釋,使用方法簽名來保證這種代碼可控性會來得更可行一些,因為注釋有可能是過時的,但方法簽名一般不太可能是陳舊的。

          2.盡量少用if choose等語句,降低維護的難度。

          Mybatis的配置SQL時,盡量少用if choose 等標簽,能用SQL實現判斷的盡量用SQL來判斷(CASE WHEN ,DECODE等),以便后期維護。否則,一旦SQL膨脹,超級惡心,如果需要調試Mybatis中的SQL,需要去除大量的判斷語句,非常麻煩。另一方面,大量的if判斷,會使生成的SQL中包含大量的空格,增加網絡傳輸的時間,也不可取。

          而且大量的if choose語句,不可避免地,每次生成的SQL會不太一致,會導致ORACLE大量的硬解析,也不可取。

          我們來看看這樣的SQL:

          SELECT * FROM T_NEWS_TEXT
          < if test="startdate !=null and startdate !='' and enddate !=null and endate !=''">
          AND PUBLISHTIME >=#{startdate} AND PUBLISHTIME <=#{enddate}
          AND PUBLISHTIME >=SYSDATE - 7 AND PUBLISHTIME <=SYSDATE
          

          這樣的if判斷,其實是完全沒有必要的,我們可以很簡單的采用DECODE來解決默認值問題:

          SELECT * FROM T_NEWS_TEXT
          WHERE PUBLISHTIME >=DECODE(#{startdate},NULL,SYSDATE-7, #{startdate}) AND PUBLISHTIME <=DECODE(#{enddate},NULL,SYSDATE,#{enddate})
          

          當然有人會想,引入CASE WHEN,DECODE會導致需要ORACLE函數解析,會拖慢SQL執行時間,有興趣的同學可以回去做一下測試,看看是否會有大的影響。就個人經驗而言,在我的開發過程,沒有發現因為函數解析導致SQL變慢的情形。影響SQL執行效率的一般情況下是JOIN、ORDER BY、DISTINCT、PARTITATION BY等這些操作,這些操作一般與表結構設計有很大的關聯。相對于這些的效率影響程度,函數解析對于SQL執行速度影響應該是可以忽略不計的。

          另外一點,對于一些默認值的賦值,像上面那條SQL,默認成當前日期什么的,其實可以完全提到Service層或Controller層做處理,在Mybatis中應該要少用這些判斷。因為,這樣的話,很難做緩存處理。如果startdate為空,在SQL上使用動態的SYSDATE,就無法確定緩存startdate日期的key應該是什么了。所以參數最好在傳遞至Mybatis之前都處理好,這樣Mybatis層也能減少部分if choose語句,同時也方便做緩存處理。

          當然不使用if choose也并不是絕對的,有時候為了優化SQL,不得不使用if來解決,比如說LIKE語句,當然一般不推薦使用LIKE,但如果存在使用的場景,盡可能在不需要使用時候去除LIKE,比如查詢文章標題,以提高查詢效率。 最好的方式是使用lucence等搜索引擎來解決這種全文索引的問題。

          總的來說,if與choose判斷分支是不可能完全去除的,但是推薦使用SQL原生的方式來解決一些動態問題,而不應該完全依賴Mybatis來完成動態分支的判斷,因為判斷分支過于復雜,而且難以維護。

          3.用XML注釋取代SQL注釋。

          Mybatis中原SQL的注釋盡量不要保留,注釋會引發一些問題,如果需要使用注釋,可以在XML中用來注釋,保證在生成的SQL中不會存在SQL注釋,從而降低問題出現的可能性。這樣做還有一個好處,就是在IDE中可以很清楚的區分注釋與SQL。

          現在來談談注釋引發的問題,我做的一個項目中,分頁組件是基于Mybatis的,它會在你寫的SQL腳本外面再套一層SELECT COUNT(*) ROWNUM_ FROM (….) 計算總記錄數,同時有另一個嵌套SELECT * FROM(…) WHERE ROWNUM > 10 AND RONNUM < 10 * 2這種方式生成分頁信息,如果你的腳本中最后一行出現了注釋,則添加的部分會成為注釋的一部分,執行就會報錯。除此之外,某些情況下也可能導致部分條件被忽略,如下面的情況:

          SELECT * FROM TEST
          WHERE COL1 > 1 -- 這里是注釋AND COL2=#{a}
          

          即使傳入的參數中存在對應的參數,實際也不會產生效果,因為后面的內容實際上是被完全注釋了。這種錯誤,如果不經過嚴格的測試,是很難發現的。一般情況下,XML注釋完全可以替代SQL注釋,因此這種行為應該可以禁止掉。

          4.盡可能使用`#{}`,而不是`${}`.

          Mybatis中盡量不要使用${},盡量這樣做很方便開發,但是有一個問題,就是大量使用會導致ORACLE的硬解析,拖慢數據庫性能,運行越久,數據庫性能會越差。對于一般多個字符串IN的處理,可以參考如下的解決方案:http://www.myexception.cn/sql/849573.html,基本可以解決大部分${}.

          關于${},另一個誤用的地方就是LIKE,我這邊還有個案例:比如一些樹型菜單,節點會設計成'01','0101',用兩位節點來區分層級,這時候,如果需要查詢01節點下所有的節點,最簡單的SQL便是:SELECT * FROM TREE WHERE ID LIKE '01%',這種SQL其實無可厚非,因為它也能用到索引,所以不需要特別的處理,直接使用就行了。但如果是文章標題,則需要額外注意了:SELECT * FROM T_NEWS_TEXT WHERE TITLE LIKE '%OSC%',這是怎么也不會用到索引的,上面說了,最好采用全文檢索。但如果離不開LIKE,就需要注意使用的方式: ID LIKE #{ID} || '%'而不是ID LIKE '

          有人覺得使用||會增加ORACLE處理的時間,我覺得不要把ORACLE看得太傻,雖然有時候確實非常傻,有空可以再總結ORACLE傻不垃圾的地方,但是稍加測試便知:這種串聯方式,對于整個SQL的解析執行,應該是微乎其微的。

          當然還有一些特殊情況是沒有辦法處理的,比如說動態注入列名、表名等。對于這些情況,則比較棘手,沒有找到比較方便的手段。由于這種情況出現的可能性會比較少,所以使用ID有人覺得使用∣∣會增加ORACLE處理的時間,我覺得不要把ORACLE看得太傻,雖然有時候確實非常傻,有空可以再總結ORACLE傻不垃圾的地方,但是稍加測試便知:這種串聯方式,對于整個SQL的解析執行,應該是微乎其微的。當然還有一些特殊情況是沒有辦法處理的,比如說動態注入列名、表名等。對于這些情況,則比較棘手,沒有找到比較方便的手段。由于這種情況出現的可能性會比較少,所以使用{}倒也不至于有什么太大的影響。當然你如果有代碼潔癖的話,可以使用ORACLE的動態執行SQL的機制Execute immediate,這樣就可以完全避免${}出現的可能性了。這樣會引入比較復雜的模型,這個時候,你就需要取舍了。

          針對于以上動態SQL所導致的問題,最激進的方式是全部采用存儲過程,用數據庫原生的方式來解決,方便開發調試,當然也會帶來問題:對開發人員會有更高的要求、存儲過程的管理等等,我這邊項目沒有采用過這種方式,這里不做更多的展開。

          5.簡單使用Mybatis。

          Mybatis的功能相對而言還是比較弱的,缺少了好多必要的輔助庫,字符串處理等等,擴展也比較困難,一般也就可能對返回值進行一些處理。因此最好僅僅把它作為單純的SQL配置文件,以及簡單的ORM框架。不要嘗試在Mybatis中做過多的動態SQL,否則會導致后續的維護非常惡心。

          幾點技巧總結;

          1、查詢很多字段時可以提出來再引入到sql語句

          提取:

          id, type, shopCouId, imgPath, fromDate, toDate, insDate, insUserId, updDate, updUserId, delFlg

          引入:

          select

          from adinfo

          where id=#{id,jdbcType=INTEGER}

          2、如果sql語句中需要使用<, >, "" 符號時,需要使用< > " 或者

           CDATA內部所有東西都會被解析器忽略
          select type, shopCouId, imgPath 
          from adinfo
          WHERE delFlg='0'
          and fromDate < #{date} and toDate >=#{date}
          

          3、緩存使用

          在增刪查改時,可以使用緩存屬性控制數據緩存

          4、可以判斷傳進來的參數,再進行操作

          and langCd=#{langCd,jdbcType=VARCHAR}

          5、可以在sql語句中直接進行加減乘除計算,模糊查詢時,需要注意使用方式

          SELECT sum(b.netCnt) + #{netCnt,jdbcType=INTEGER}
          FROM collectcnt b
          WHERE b.shopId=#{shopId,jdbcType=INTEGER}
          AND b.delflg='0' 
          newCnt=newCnt + 1,
          netCnt=netCnt +1,
          sumCnt=sumCnt + 1,
          AND o.oTime LIKE CONCAT('%',#{sdate},'%')
          

          MyBatis把int類型的0處理成空串’’和mysql處理空串’’為0的問題

          當數據庫字段類型是整數,如果參數變量為空字符串或者NULL,Mybatis會自動將參數賦值0,所以如果要判斷整數參數的多種狀態在傳遞數值到Mapper之前就要判斷是否為空字符串和NULL并將相應的狀態數值賦值給該參數,否則參數值等于空字符串、NULL和0得到的結果是一樣的。
          一般情況下,涉及到int類型的操作的時候,在Service中會統一把數字類型先變成字符串類型,然后再傳遞到Mapper中操作。
          


          案例七:

          使用mybatis 進行批量insert的時候 會自動封裝成一個map key是list 要存的數據變成了數組 需要注意在xml里面如果使用自己定義的collection要在傳參時定義一個mapkey是自己定義的變量名哦。

          在使用resultMap的時候,要把ID寫在第一行,否則的話,就會報錯。

          優缺點

          總結下mybatis的優缺點,以便大家對于mybatis的了解能更全面些。但我所說的優缺點,僅是我個人總結并結合使用體驗后得出的結果,并不能代表大眾想法,因此才以“淺談”作為文章標題。如果大家的見解與我不同,歡迎積極提出來一塊討論,我也借以彌補自己認識的不足和短見。

          優點:

          1. 易于上手和掌握。
          2. sql寫在xml里,便于統一管理和優化。
          3. 解除sql與程序代碼的耦合。
          4. 提供映射標簽,支持對象與數據庫的orm字段關系映射
          5. 提供對象關系映射標簽,支持對象關系組建維護
          6. 提供xml標簽,支持編寫動態sql。

          缺點:

          1. sql工作量很大,尤其是字段多、關聯表多時,更是如此。
          2. sql依賴于數據庫,導致數據庫移植性差。
          3. 由于xml里標簽id必須唯一,導致DAO中方法不支持方法重載。
          4. 字段映射標簽和對象關系映射標簽僅僅是對映射關系的描述,具體實現仍然依賴于sql。(比如配置了一對多Collection標簽,如果sql里沒有join子表或查詢子表的話,查詢后返回的對象是不具備對象關系的,即Collection的對象為null)
          5. DAO層過于簡單,對象組裝的工作量較大。
          6. 不支持級聯更新、級聯刪除。
          7. 編寫動態sql時,不方便調試,尤其邏輯復雜時。
          8. 8 提供的寫動態sql的xml標簽功能簡單(連struts都比不上),編寫動態sql仍然受限,且可讀性低。
          9. 若不查詢主鍵字段,容易造成查詢出的對象有“覆蓋”現象。
          10. 參數的數據類型支持不完善。(如參數為Date類型時,容易報沒有get、set方法,需在參數上加@param)
          11. 多參數時,使用不方便,功能不夠強大。(目前支持的方法有map、對象、注解@param以及默認采用012索引位的方式)
          12. 緩存使用不當,容易產生臟數據。

          總結:

          mybatis的優點其實也是mybatis的缺點,正因為mybatis使用簡單,數據的可靠性、完整性的瓶頸便更多依賴于程序員對sql的使用水平上了。sql寫在xml里,雖然方便了修改、優化和統一瀏覽,但可讀性很低,調試也非常困難,也非常受限,無法像jdbc那樣在代碼里根據邏輯實現復雜動態sql拼接。mybatis簡單看就是提供了字段映射和對象關系映射的jdbc,省去了數據賦值到對象的步驟而已,除此以外并無太多作為,不要把它想象成hibernate那樣強大,簡單小巧易用上手,方便瀏覽修改sql就是它最大的優點了。

          mybatis適用于小型且程序員能力較低的項目和人群使用,對于中大型項目來說我并不推薦使用,如果覺得hibernate效率低的話(實際上也是使用不當所致,hibernate是實際上是不適用于擁有高負載的工程項目),還不如直接用spring提供的jdbc簡單框架(Template),同樣支持對象映射。


          主站蜘蛛池模板: 日韩一区二区三区无码影院| 日韩一区二区三区四区不卡| 国产成人久久精品麻豆一区| 国产小仙女视频一区二区三区| 中文字幕一区二区三匹| 日韩精品一区二区三区色欲AV | 亚洲色一区二区三区四区| 性色AV一区二区三区天美传媒| 相泽南亚洲一区二区在线播放 | 久久一区二区精品综合| 亚洲AV无码一区二区三区在线| 天堂Av无码Av一区二区三区| 波多野结衣中文一区| 亚洲AV无码一区二区二三区软件| 精品国产一区二区三区麻豆 | 在线观看日韩一区| 中文字幕一区二区三区四区| 香蕉久久av一区二区三区| 亚洲av无一区二区三区| 日本一区视频在线播放| 亚洲视频一区调教| 精品国产一区二区三区香蕉事 | 四虎精品亚洲一区二区三区| 久久婷婷久久一区二区三区| 无码人妻精一区二区三区 | 色老头在线一区二区三区| 春暖花开亚洲性无区一区二区| 中文字幕一区二区人妻性色 | 亚洲一区二区三区日本久久九| 精品国产日韩一区三区| 亚洲电影一区二区| 国产成人精品一区在线 | 久久精品动漫一区二区三区| 日韩免费无码一区二区视频| 午夜精品一区二区三区免费视频| 国产主播一区二区三区在线观看| 91精品一区二区综合在线| 中文字幕一区视频一线| 精品国产天堂综合一区在线| 国产91大片精品一区在线观看| 肥臀熟女一区二区三区|