Java開發熟手該小心的11個錯誤

我十分驚訝的發現,我最近的一篇文章——《Java開發者寫SQL時常犯的10個錯誤》——最近在個人博客個人合做夥伴DZone上很是的受歡迎。(這篇博客)的流行程度說明了幾件事: html

  • SQL在專業的Java開發中多麼重要。
  • 基本的SQL知識被忘掉(的狀況)廣泛存在。
  • 經過embracing SQL,你就能瞭解像 jOOQMyBatis這樣的以SQL爲中心的庫正好反應了市場的須要。 使人驚喜的是有用戶提到了我博客上貼的一篇「SLICK’s mailing list」,SLICK是Scala中的一個不以SQL爲中心的數據庫訪問庫,像LINQ(還有LINQ-to-SQL),它側重語言整合,而不是SQL語句的產生。

不管如何,我以前倉促列出的常見錯誤還沒列完。所以我爲你另外準備了10個沒那麼常見的,但Java開發者在寫SQL語句時一樣愛犯的錯誤。 java

一、不用PreparedStatements

有意思的是,在JDBC出現了許多年後的今天,這個錯誤依然出如今博客、論壇和郵件列表中,即使要記住和理解它是一件很簡單的事。開發者不使用PreparedStatements的緣由可能有以下幾個: sql

  • 他們對PreparedStatements不瞭解
  • 他們認爲使用PreparedStatements太慢了
  • 他們認爲寫PreparedStatements太費力

來吧,咱們來破除上面的謠言。96%的案例中,用PreparedStatement比靜態聲明語句更好。爲何呢?就是下面這些簡單的緣由: 數據庫

  • 使用內聯綁定值(inlining bind values)能夠從源頭上避免糟糕的語句引發語法錯誤。
  • 使用內聯綁定值能夠避免糟糕的語句引發的SQL注入漏洞。
  • 當插入更多「複雜的」數據類型(好比時間戳、二進制數據等等)時,能夠避免邊緣現象(edge-cases)。
  • 你若保持PreparedStatements的鏈接開啓狀態而不立刻關閉,只要從新綁定新值就能夠進行復用。
  • 你能夠在更多複雜的數據庫裏使用adaptive cursor sharing——自適應遊標共享(Oracle的說法)。這能夠幫你在每次新設定綁定值時阻止SQL語句硬解析。

(譯者注:硬解析的弊端。硬解析即整個SQL語句的執行須要完徹底全的解析,生成執行計劃。而硬解析,生成執行計劃須要耗用CPU資源,以及SGA資源。在此不得不提的是對庫緩存中 閂的使用。閂是鎖的細化,能夠理解爲是一種輕量級的串行化設備。當進程申請到閂後,則這些閂用於保護共享內存的數在同一時刻不會被兩個以上的進程修改。在 硬解析時,須要申請閂的使用,而閂的數量在有限的狀況下須要等待。大量的閂的使用由此形成須要使用閂的進程排隊越頻繁,性能則逾低下) express

某些特殊狀況下你須要對值進行內聯綁定,這是爲了給基於成本的性能優化器提示該查詢將要涉及的數據集。典型的狀況是用「常量」判斷: 緩存

  • DELETED = 1
  • STATUS = 42

而不該該用一個「變量」判斷: 性能優化

  • FIRST_NAME LIKE 「Jon%」
  • AMOUNT > 19.95

要注意的是,現代數據庫已經實現了綁定數據窺探(bind-variable peeking)。所以,默認狀況下,你也能夠爲你全部的查詢參數使用綁定值。在你寫嵌入的JPQL或嵌入的SQL時,用JPA CriteriaQuery或者jOOQ這類高層次的API能夠很容易也很清晰的幫你生成PreparedStatements語句並綁定值。 mybatis

更多的背景資料: oracle

解決方案: 性能

默認狀況下,老是使用PreparedStatements來代替靜態聲明語句,而永遠不要在你的SQL語句嵌入內聯綁定值。

二、返回太多列

這個錯誤發生的很是頻繁,它不光會影響你的數據庫執行計劃,也會對你的Java應用形成很差的影響。讓咱們先看看對後者的影響:

對Java程序的不良影響:

如 果你爲了知足不一樣DAO層之間的數據複用而select *或者默認的50個列,這樣將會有大量的數據從數據庫讀入到JDBC結果集中,即便你不從結果集讀取數據,它也被傳遞到了線路上並被JDBC驅動器加載到 了內存中。若是你知道你只須要2-3列數據的話,這就形成了嚴重的IO和內存的浪費。

這個(問題的嚴重性)都是顯而易見的,要當心……

對數據庫執行計劃的不良影響:

這 些影響事實上可能比對Java應用的影響還要嚴重。當複雜的數據庫要針對你的查詢請求計算出最佳執行計劃時,它會進行大量的SQL轉換(SQL transformation )。還好,請求中的一部分能夠被略去,由於它們對SQL連映射或過濾條件起不了什麼做用。我最近寫了一篇博客來說述這個問題:元數據模式會對Oracle查詢轉換產生怎樣的影響

如今,給你展現一個錯誤的例子。想想有兩個視圖的複雜查詢:

1
2
3
4
SELECT*
FROM customer_view c
JOIN order_view o
ON c.cust_id = o.cust_id

每一個關聯了上述關聯表引用的視圖也可能再次關聯其餘表的數據,像 CUSTOMER_ADDRESS、ORDER_HISTORY、ORDER_SETTLEMENT等等。進行select * 映射時,你的數據庫除了把全部鏈接表都加載進來之外別無選擇,實際上,你惟一感興趣的數據可能只有這些:

1
2
3
4
SELECTc.first_name, c.last_name, o.amount
FROM customer_view c
JOIN order_view o
ON c.cust_id = o.cust_id

一個好的數據庫會在轉換你的SQL語句時自動移除那些不須要的鏈接,這樣數據庫就只須要較少的IO和內存消耗。

解決方案:

永遠不要用select *(這樣的查詢)。也不要在執行不一樣請求時複用相同的映射。儘可能嘗試減小映射到你所真正須要的數據。

須要注意的是,想在對象-關係映射(ORMs)上達成這個目標有些難。

三、把JOIN當作了SELECT的子句

對於性能或SQL語句的正確性來講,這不算錯。可是無論如何,SQL開發者應該意識到JOIN子句不是SELECT語句的一部分。SQL standard 1992 定義了表引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
6.3 <table reference>
 
<table reference> ::=
<table name> [ [ AS ] <correlation name>
[ <left paren> <derived column list> <right paren> ] ]
| <derived table> [ AS ] <correlation name>
[ <left paren> <derived column list> <right paren> ]
| <joined table>
 
7.4 <from clause>
 
<from clause> ::=
FROM <table reference> [ { <comma> <table reference> }... ]
 
7.5 <joined table>
 
<joined table> ::=
<cross join>
| <qualified join>
| <left paren> <joined table> <right paren>
 
<cross join> ::=
<table reference> CROSS JOIN <table reference>
 
<qualified join> ::=
<table reference> [ NATURAL ] [ <join type> ] JOIN
<table reference> [ <join specification> ]

關聯數據庫是以表爲中心的。許多的操做的某方面都是執行在物理表、鏈接表或派生表上的。爲了有效的寫出SQL語句,理解SELECT … FROM子句是以「,」分割表引用是很是重要的。

基於表引用(table references)的複雜性,一些數據庫也接受其它類型的複雜的表引用(table references),像INSERT、UPDATE、DELETE、MERGE。看看Oracle實例手冊,裏面解釋瞭如何建立可更新的視圖。

解決方案:

必定要考慮到,通常說來,FROM子句也是一個表引用(table references)。若是你寫了JOIN子句,要考慮這個JOIN子句是這個複雜的表引用的一部分:

1
2
3
4
SELECTc.first_name, c.last_name, o.amount
FROMcustomer_view c
JOINorder_view o
ONc.cust_id = o.cust_id

四、使用ANSI 92標準以前鏈接語法

我 們已經說清了表引用是怎麼工做的(看上一節),所以咱們應該達成共識,不論花費什麼代價,都應該避免使用ANSI 92標準以前的語法。就執行計劃而言,使用JOIN…ON子句或者WHERE子句來做鏈接謂語沒有什麼不一樣。但從可讀性和可維護性的角度看,在過濾條 件判斷和鏈接判斷中用WHERE子句會陷入不可自拔的泥沼,看看這個簡單的例子:

1
2
3
4
5
6
SELECTc.first_name, c.last_name, o.amount
FROM customer_view c,
order_view o
WHERE o.amount > 100
AND   c.cust_id = o.cust_id
AND   c.language ='en'

你能找到join謂詞麼?若是咱們加入數十張表呢?當你使用外鏈接專有語法的時候會變得更糟,就像Oracle的(+)語法裏講的同樣。

解決方案:

必定要用ANSI 92標準的JOIN語句。不要把JOIN謂詞放到WHERE子句中。用ANSI 92標準以前的JOIN語法沒有半點好處。

五、使用LIKE斷定時忘了ESCAPE

SQL standard 1992 指出like斷定應該以下:

1
2
3
4
5
8.5 <like predicate>
 
<like predicate> ::=
<match value> [ NOT ] LIKE <pattern>
[ ESCAPE <escape character> ]

當容許用戶對你的SQL查詢進行參數輸入時,就應該使用ESCAPE關鍵字。儘管數據中含有百分號(%)的狀況很罕見,但下劃線(_)仍是很常見的:

1
2
3
SELECT*
FROM t
WHERE t.xLIKE'some!_prefix%'ESCAPE'!'

解決方案:

使用LIKE斷定時,也要使用合適的ESCAPE

六、認爲 NOT (A IN (X, Y)) 和 IN (X, Y) 的布爾值相反

對於NULLs,這是一個舉足輕重的細節!讓咱們看看 A IN (X, Y) 真正意思吧:

A IN (X, Y)
is the same as    A = ANY (X, Y)
is the same as    A = X OR A = Y

When at the same time, NOT (A IN (X, Y)) really means:

一樣的,NOT (A IN (X, Y))的真正意思:

NOT (A IN (X, Y))
is the same as    A NOT IN (X, Y)
is the same as    A != ANY (X, Y)
is the same as    A != X AND A != Y

看起來和以前說的布爾值相反同樣?其實不是。若是X或Y中任何一個爲NULL,NOT IN 條件產生的結果將是UNKNOWN,可是IN條件可能依然會返回一個布爾值。

或者換種說話,當 A IN (X, Y) 結果爲TRUE或FALSE時,NOT(A IN (X, Y)) 結果爲依然UNKNOWN而不是FALSE或TRUE。注意了,若是IN條件的右邊是一個子查詢,結果依舊。

不信?你本身看SQL Fiddle 去。它說了以下查詢給不出結果:

1
2
3
4
5
SELECT1
WHERE   1IN(NULL)
UNIONALL
SELECT2
WHERENOT(1IN(NULL))

更多細節能夠參考個人上一篇博客,上面寫了在同區域內不兼容的一些SQL方言。

解決方案:

當涉及到可爲NULL的列時,注意NOT IN條件。

七、認爲NOT (A IS NULL)和A IS NOT NULL是同樣的

沒錯,咱們記得處理NULL值的時候,SQL實現了三值邏輯。這就是咱們能用NULL條件來檢測NULL值的緣由。對麼?沒錯。

但在NULL條件容易遺漏的狀況下。要意識到下面這兩個條件僅僅在行值表達式(row value expressions)爲1的時候才相等:

NOT (A IS NULL)
is not the same as A IS NOT NULL

若是A是一個大於1的行值表達式(row value expressions),正確的表將按照以下方式轉換:

  • 若是A的全部值爲NUll,A IS NULL爲TRUE
  • 若是A的全部值爲NUll,NOT(A IS NULL) 爲FALSE
  • 若是A的全部值都不是NUll,A IS NOT NULL 爲TRUE
  • 若是A的全部值都不是NUll,NOT(A IS NOT NULL)  爲FALSE

在個人上一篇博客能夠了解到更多細節。

解決方案:

當使用行值表達式(row value expressions)時,要注意NULL條件不必定能達到預期的效果。

八、不用行值表達式

行值表達式是SQL一個很棒的特性。SQL是一個以表格爲中心的語言,表格又是以行爲中心。經過建立能在同等級或行類型進行比較的點對點行模型,行值表達式讓你能更容易的描述複雜的斷定條件。一個簡單的例子是,同時請求客戶的姓名

1
2
3
SELECTc.address
FROM customer c,
WHERE(c.first_name, c.last_name) = (?, ?)

能夠看出,就將每行的謂詞左邊和與之對應的右邊比較這個語法而言,行值表達式的語法更加簡潔。特別是在有許多獨立條件經過AND鏈接的時候就特別有效。行值表達式容許你將相互聯繫的條件放在一塊兒。對於有外鍵的JOIN表達式來講,它更有用:

1
2
3
4
SELECTc.first_name, c.last_name, a.street
FROM customer c
JOIN address a
ON (c.id, c.tenant_id) = (a.id, a.tenant_id)

不幸的是,並非全部數據庫都支持行值表達式。但SQL標準已經在1992對行值表達式進行了定義,若是你使用他們,像Oracle或Postgres這些的複雜數據庫可使用它們計算出更好的執行計劃。在Use The Index, Luke這個頁面上有解析。

解決方案

無論幹什麼均可以使用行值表達式。它們會讓你的SQL語句更加簡潔高效。

九、不定義足夠的限制條件(constraints

我又要再次引用Tom Kyte 和 Use The Index, Luke 了。對你的元數據使用限制條件不能更讚了。首先,限制條件能夠幫你防止數據質變,光這一點就頗有用。但對我來講更重要的是,限制條件能夠幫助數據庫進行SQL語句轉換,數據庫能夠決定。

  • 哪些值是等價的
  • 哪些子句是冗餘的
  • 哪些子句是無效的(例如,會返回空值的語句)

有些開發者可能認爲限制條件會致使(數據庫)變慢。但相反,除非你插入大量的數據,對於大型操做是你能夠禁用限制條件,或用一個無限制條件的臨時「載入表」,線下再把數據轉移到真實的表中。

解決方案:

儘量定義足夠多的限制條件(constraints)。它們將幫你更好的執行數據庫請求。

十、認爲50ms是一個快的查詢速度

NoSQL的炒做依然在繼續,許多公司認爲它們像Twitter或Facebook同樣須要更快、擴展性更好的解決方案,想脫離ACID和關係模型橫向擴展。有些可能會成功(好比Twitter或Facebook),而其餘的也許會走入誤區:

看這篇文章:https://twitter.com/codinghorror/status/347070841059692545

對於那些仍被迫(或堅持)使用關係型數據 庫的公司,請不要自欺欺人的認爲:「如今的關係型數據庫很慢,其實它們是被天花亂墜的宣傳弄快的」。實際上,它們真的很快,解析20Kb查詢文檔,計算 2000行執行計劃,如此龐大的執行,所需時間小於1ms,若是你和數據管理員(DBA)繼續優化調整數據庫,就能獲得最大限度的運行。

它們會變慢的緣由有兩種:一是你的應用濫用流行的ORM;二是ORM沒法針對你複雜的查詢邏輯產生快的SQL語句。遇到這種狀況,你就要考慮選擇像 JDBCjOOQ 或MyBatis這樣的更貼近SQL核心,能更好的控制你的SQL語句的API。

所以,不要認爲查詢速度50ms是很快或者能夠接受的。徹底不是!若是你程序運行時間是這樣的,請檢查你的執行計劃。這種潛在危險可能會在你執行更復雜的上下文或數據中爆發。

總結

SQL頗有趣,同時在各類各樣的方面也很微妙。正如個人關於10個錯誤的博客所展現的。跋山涉水也要掌握SQL是一件值得作的事。數據是你最有價值的資產。帶着尊敬的心態對待你的數據才能寫出更好的SQL語句。

相關文章
相關標籤/搜索