說來,mysql數據庫是我們項目中用的比較多的庫,ORM工具喜歡采用細粒度的MyBatis。這里面就這么引出了兩者之間的故事!
首先,說改字段吧,將一個表中的varchar字段改為enum字段。如下:
mysql> desc ucc_purchase_status; +-------------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | status_type | varchar(64) | NO | | NULL | | | timestamp | datetime | NO | | NULL | | | purchase_id | int(11) | YES | MUL | NULL | | +-------------+-------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec)
這個表,主要是用來記錄訂單的狀態。因為業務場景,這個訂單的狀態是有限的,目前只有"下單","付款","發貨","收貨","撤單","退貨"。於是想將其修改成enum類型。
如下操作,得到錯誤了!!!
mysql> alter table ucc_purchase_status modify status_type enum("xd","fk","fh","sh","cd","th") not null default "xd"; ERROR 1265 (01000): Data truncated for column 'status_type' at row 1
這個錯誤,是什么意思呢?看到data truncated,應該想到什么呢? 通常和數據記錄的內容有關系! 是不是因為我改類型后,默認值與表中當前的值有沖突呢?
帶着這個疑問,看了下這個表中的內容:
mysql> select * from ucc_purchase_status; +----+-------------+---------------------+-------------+ | id | status_type | timestamp | purchase_id | +----+-------------+---------------------+-------------+ | 1 | 下單 | 2017-05-22 14:32:04 | 1 | | 2 | 發貨 | 2017-05-23 17:32:44 | 2 | | 3 | 發貨 | 2017-05-24 11:01:19 | 2 | | 4 | 收貨 | 2017-05-22 14:18:23 | 3 | +----+-------------+---------------------+-------------+ 4 rows in set (0.00 sec)
的確,我這個表里面的status_type的值,的確和枚舉的值,是不同的,真是這個原因造成的么?試試!
mysql> alter table ucc_purchase_status modify status_type enum("下單","付款","發貨","收貨","撤單","退貨") not null default "下單"; Query OK, 4 rows affected (0.04 sec) Records: 4 Duplicates: 0 Warnings: 0
呵呵,看來,這個是真的,這個分析是成立的! 改后的表結構:
mysql> desc ucc_purchase_status; +-------------+-------------------------------------------------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-------------------------------------------------------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | status_type | enum('下單','付款','發貨','收貨','撤單','退貨') | NO | | 下單 | | | timestamp | datetime | NO | | NULL | | | purchase_id | int(11) | YES | MUL | NULL | | +-------------+-------------------------------------------------------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec)
這個數據類型的變化,對應的mapper文件也要修改!用mybatisGenerator工具生成mapper數據!
這個是字段改成enum之前的mapper文件部分:
<resultMap id="BaseResultMap" type="com.tg.ecs.ucc.model.UccPurchaseStatus" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="status_type" property="statusType" jdbcType="VARCHAR" /> <result column="timestamp" property="timestamp" jdbcType="TIMESTAMP" /> <result column="purchase_id" property="purchaseId" jdbcType="INTEGER" /> </resultMap>
下面這個,是status_type改成enum后的mapper文件部分:
<resultMap id="BaseResultMap" type="com.tg.ecs.ucc.model.UccPurchaseStatus"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="status_type" jdbcType="CHAR" property="statusType" /> <result column="timestamp" jdbcType="TIMESTAMP" property="timestamp" /> <result column="purchase_id" jdbcType="INTEGER" property="purchaseId" /> </resultMap>
好,到此,數據都改的差不多了,當然,dao里面的相關數據也都已經修正了。最后,就是mapper里面的sql查詢,做相應的修改。之前對status_type的傳值,都是hard code寫死的,但是呢,這個業務場景,我希望查詢數據,是基於狀態的,比如付款的,未付款的,等等,邏輯都一樣,就是狀態傳入的值不一樣。於是,寫一個通用的sql,通過參數進行過濾,是非常容易想到的方案!
這里,就涉及到mybatis增刪改查中dao接口函數中傳參數的問題了。
根據mybatis的官方文檔,支持三種類型的傳參模式:
1. 通過位置序號進行參數映射(序號從0開始,從函數參數列表中,自左向右依次遞增,0,1,2,3)
2. 通過注解@Param("xxx")來進行參數名映射,其中的xxx字符串代表mapper中sql里面的傳入參數。
3. 通過map對參數進行裝載,通過KV的方式,K就是map的key,對應mapper文件里的sql中的傳入參數的變量名。
對這三個方式,做一個簡單的概括,也是mybatis的官方說法:任何參數傳遞,最終都將轉化為map的形式,傳入到mybatis的解析系統。默認是用key作為sql中的參數名,除非指定@Param進行重命名!
第一種模式,通過位置序號,比較不建議,因為在mapper的sql中不能顧名思義。
從這三個模式,你是不是覺得比較簡單?mybatis里面也只是這么簡單的描述!但是沒有說這三個情況的組合情況!你有沒有想過呢?至少我在我的應用場景,就上面的訂單問題上,我有這個考慮!
下面請看我用的第2種模式:
dao的interface函數: List<PurchaseElement> findPElementByCustomerInfoAndStatus(@Param("uid") Integer id, @Param("start") Integer start, @Param("limit") Integer limit, @Param("list") List<String>stats);
這個其實比較簡單,對應的mapper的sql如下(注意,紅色部分名稱的對應關系):
<!-- 根據參數用戶ID,訂單狀態兩個參數,進行過濾 --> <select id="findPElementByCustomerInfoAndStatus" resultMap="purchaseElementResultMap"> select up.id as up_id, ups.status_type as ups_status_type, uua.id as uua_id, uua.username as uua_username, uua.address as uua_address, uua.mobile as uua_mobile, uua.zipcode as uua_zipcode, mp.name as mp_name, mp.price as mp_price, (select count(tupp.product_id) from ucc_purchase_product tupp where tupp.purchase_id = up.id and tupp.product_id = upp.product_id) as upp_quantity from ucc_purchase as up left join ucc_purchase_product as upp on upp.purchase_id = up.id left join mcc_product as mp on mp.id = upp.product_id left join ucc_purchase_status as ups on ups.purchase_id = upp.purchase_id left join ucc_user_address as uua on uua.id = up.address_id where ups.status_type is not null and ups.status_type != '' and (uua.id is not null and uua.id != '') and (uua.username is not null and uua.username != '') and (uua.address is not null and uua.address != '') and (uua.mobile is not null and uua.mobile != '') and (mp.name is not null and mp.name != '') and (mp.price is not null and mp.price !='') and up.customer_id = #{uid, jdbcType=INTEGER} and (ups.status_type in <foreach item="st" collection="list" index="idx" open="(" separator="," close=")"> #{st, jdbcType=CHAR} </foreach> ) order by up.timestamp desc <if test="start != null and limit!=null"> limit #{start}, #{limit} </if> </select>
對應的controller里面的業務邏輯代碼如下:
@GET @Path("/load/paid") public String loadPaid(@Context HttpServletRequest req){ SysUser su = infos.getCurrentUser(); DataTablePager.generatePager(req, su); Integer id = su.getId().intValue(); Integer start = su.getStart(); Integer limit = su.getLimit(); List<PurchaseElement> list = pes.findPElementByCustomerInfoAndStatus(id, start, limit, pes.paidStatus()); int count = pes.findAllPElementCountByStatus(id, pes.paidStatus()); String sEcho = req.getParameter("sEcho"); return DataTablePager.getPageJson(list, count, sEcho); }
至於第三種,構建map這個就不用說了,將要傳遞的參數都寫入一個map里面,用的時候通過map里面的key的名稱取變量就可以了。
這里,我要說的是復雜類型,傳遞的參數,含有javaBean以及集合類型兩個參數。當然,參數只有集合或者只有javaBean都比較簡單,只有一個javaBean參數,mybatis會轉化為map形式,但是對於既有javaBean,又有其他參數,例如我這里的List類型,會如何呢?
下面的經歷,可以看出mybatis的強大!先看dao的接口:
List<PurchaseElement> findPElementByCustomerBeanAndStatus(@Param("su") SysUser su, @Param("list") List<String>stats);
再看看mapper文件:
<!-- 根據參數用戶ID,訂單狀態兩個參數,進行過濾 --> <select id="findPElementByCustomerBeanAndStatus" resultMap="purchaseElementResultMap"> select up.id as up_id, ups.status_type as ups_status_type, uua.id as uua_id, uua.username as uua_username, uua.address as uua_address, uua.mobile as uua_mobile, uua.zipcode as uua_zipcode, mp.name as mp_name, mp.price as mp_price, (select count(tupp.product_id) from ucc_purchase_product tupp where tupp.purchase_id = up.id and tupp.product_id = upp.product_id) as upp_quantity from ucc_purchase as up left join ucc_purchase_product as upp on upp.purchase_id = up.id left join mcc_product as mp on mp.id = upp.product_id left join ucc_purchase_status as ups on ups.purchase_id = upp.purchase_id left join ucc_user_address as uua on uua.id = up.address_id where ups.status_type is not null and ups.status_type != '' and (uua.id is not null and uua.id != '') and (uua.username is not null and uua.username != '') and (uua.address is not null and uua.address != '') and (uua.mobile is not null and uua.mobile != '') and (mp.name is not null and mp.name != '') and (mp.price is not null and mp.price !='') and up.customer_id = #{su.id, jdbcType=INTEGER} and (ups.status_type in <foreach item="st" collection="list" index="idx" open="(" separator="," close=")"> #{st, jdbcType=CHAR} </foreach> ) order by up.timestamp desc <if test="start != null and limit!=null"> limit #{su.start}, #{su.limit} </if> </select>
controller里面的代碼如下:
@GET @Path("/load/paid") public String loadPaid(@Context HttpServletRequest req){ SysUser su = infos.getCurrentUser(); DataTablePager.generatePager(req, su); Integer id = su.getId().intValue(); List<PurchaseElement> list = pes.findPElementByCustomerBeanAndStatus(su, pes.paidStatus()); int count = pes.findAllPElementCountByStatus(id, pes.paidStatus()); String sEcho = req.getParameter("sEcho"); return DataTablePager.getPageJson(list, count, sEcho); }
可以看到,其中SysUser是一個用戶數據的定義,以及后端分頁信息的繼承。
運行后,居然報錯!!!!
五月 26, 2017 4:33:04 下午 org.apache.catalina.core.StandardWrapperValve invoke 嚴重: Servlet.service() for servlet [default] in context with path [/ecs] threw exception [org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'start' not found. Available parameters are [param1, param2, su, list]] with root cause org.apache.ibatis.binding.BindingException: Parameter 'start' not found. Available parameters are [param1, param2, su, list] at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:165) at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextAccessor.getProperty(DynamicContext.java:123) at org.apache.ibatis.ognl.OgnlRuntime.getProperty(OgnlRuntime.java:1657) at org.apache.ibatis.ognl.ASTProperty.getValueBody(ASTProperty.java:92)
仔細查詢,涉及findPElementByCustomerBeanAndStatus函數調用邏輯的所有函數,以及mapper文件,最后,發現mapper文件中,的確有個start是獨立的。就是那個動態sql if中判斷分頁數據的地方有問題。應該加上命名空間。
對比上面運行出錯的mapper的內容,正確的應該是如下這個樣子的:
<!-- 根據參數用戶ID,訂單狀態兩個參數,進行過濾 --> <select id="findPElementByCustomerBeanAndStatus" resultMap="purchaseElementResultMap"> select up.id as up_id, ups.status_type as ups_status_type, uua.id as uua_id, uua.username as uua_username, uua.address as uua_address, uua.mobile as uua_mobile, uua.zipcode as uua_zipcode, mp.name as mp_name, mp.price as mp_price, (select count(tupp.product_id) from ucc_purchase_product tupp where tupp.purchase_id = up.id and tupp.product_id = upp.product_id) as upp_quantity from ucc_purchase as up left join ucc_purchase_product as upp on upp.purchase_id = up.id left join mcc_product as mp on mp.id = upp.product_id left join ucc_purchase_status as ups on ups.purchase_id = upp.purchase_id left join ucc_user_address as uua on uua.id = up.address_id where ups.status_type is not null and ups.status_type != '' and (uua.id is not null and uua.id != '') and (uua.username is not null and uua.username != '') and (uua.address is not null and uua.address != '') and (uua.mobile is not null and uua.mobile != '') and (mp.name is not null and mp.name != '') and (mp.price is not null and mp.price !='') and up.customer_id = #{su.id, jdbcType=INTEGER} and (ups.status_type in <foreach item="st" collection="list" index="idx" open="(" separator="," close=")"> #{st, jdbcType=CHAR} </foreach> ) order by up.timestamp desc <if test="su.start != null and su.limit!=null"> limit #{su.start}, #{su.limit} </if> </select>
到此,有必要補充一下,上述resultMap的內容:
<mapper namespace="com.tg.ecs.ucc.dao.PurchaseElementMapper" > <resultMap id="purchaseElementResultMap" type="com.tg.ecs.ucc.model.PurchaseElement" > <id column="up_id" property="purchaseId" jdbcType="INTEGER"/> <result column="ups_status_type" property="statusType" jdbcType="VARCHAR"/> <!-- 特別注意:association以及collection這樣子的標簽,必須放在resultMap的最后面,且association在collection的前面 --> <association property="receivedBy" column="up_id" javaType="com.tg.ecs.ucc.model.ConsigneeElement"> <id column="uua_id" property="aid" /> <result column="uua_username" property="name" /> <result column="uua_address" property="address" /> <result column="uua_mobile" property="mobile" /> <result column="uua_zipcode" property="zipcode"/> </association> <!--一個訂單號對應多個產品 --> <collection property="products" column="up_id" ofType="com.tg.ecs.ucc.model.PurchaseProduct"> <result column="mp_name" property="productName"/> <result column="mp_price" property="productPrice"/> <result column="upp_quantity" property="quantity"/> </collection> </resultMap>
注意,其中的紅色部分,若不按照其規則配置,將會出現下面的錯誤:
The content of element type "resultMap" must match "(constructor?,id*,result*,association*,collection*,discriminator?)".
總結一下:
基於mybatis的參數傳遞功能,mybatis是支持任何參數的傳遞的,不僅是基礎類型(String,Integer等)的參數傳遞,也支持javaBean類型的傳遞,同時,也支持各種類型的組合傳遞。只是使用的過程中,dao接口函數中參數基於@Param注解進行重命名,方便和mapper中sql語句的變量映射。最重要的一點,要注意:若參數中是javaBean的話,且有多個入口參數,就要注意變量的命名空間問題,我上面遇到的錯誤,找不到start變量(其實,start是SysUser里面的一個成員變量名),就是這個原因造成的!
另外,插曲一段:
在做訂單狀態傳遞進入sql的過程中,in 后面的foreach的使用,遇到了一點問題。故事是這樣的,開始我的sql是這樣的:
<foreach item="st", collection="list", index="idx", open="(", separator=",", close=")"> #{st, jdbcType=CHAR} </foreach>
這個時候,我的mapper文件中,總是報錯:
Element type "foreach" must be followed by either attribute specifications, ">" or "/>".
仔細核對mybatis的技術手冊,才發現,粗心大意造成了這個錯誤,因為屬性字段之間不應該有逗號!是用“空格”,切記!
正確的寫法應該是這樣的:
<foreach item="st" collection="list" index="idx" open="(" separator="," close=")"> #{st, jdbcType=CHAR} </foreach>
好了,今天,這個博文,就寫到這里吧! 下一篇,將總結一下foreach中的collection以及index的使用,因為我看到好多人在糾結這兩個字段的用法!