Groovy的SQL模塊提供了對JDBC的抽象,讓我們使用JDBC更簡單,相關類在groovy.sql
包下。本文參考自Working with a relational database,一些代碼引用了官方文檔,需要了解詳細信息請參見原文。
和原文一樣,為了簡單這里使用嵌入式數據庫HSQLDB,連接時在內存中創建一個數據庫。數據庫驅動可以使用Maven或Gradle導入,也可以使用Groovy自帶的Grape依賴管理器。不過在Intellij IDEA上,下面的代碼有時候無法編譯,說是找不到數據庫驅動。這時候可以使用Gradle等工具管理依賴。
Groovy SQL的所有操作都在Sql類中,我們調用Sql的newInstance方法,傳遞URL、用戶名、密碼等參數即可連接到數據庫。這種方式需要自己手動調用close方法關閉數據庫。如果希望Groovy自動關閉連接,可以使用withInstance方法,所有操作都在該方法的參數閉包中完成,之后會自動關閉連接。
@GrabResolver(name = 'aliyun', root = 'http://maven.aliyun.com/nexus/content/groups/public/')@GrabConfig(systemClassLoader = true)@Grab(group = 'org.hsqldb', module = 'hsqldb', version = '2.3.4')class SqlDatabase { static void main(String[] args) { def sql = setUpDatabase() } static Sql setUpDatabase() { def url = 'jdbc:hsqldb:mem:test' def user = 'sa' def passWord = '' def driver = 'org.hsqldb.jdbcDriver' def sql = Sql.newInstance(url, user, password, driver) return sql }如果使用數據源創建連接??梢灾苯訉祿磦鬟f給Sql的構造方法,即可由數據源創建連接。
def dataSource = new JDBCDataSource( database: 'jdbc:hsqldb:mem:yourDB', user: 'sa', password: '')def sql = new Sql(dataSource)我們可以使用Sql的execute方法執行SQL語句。這里創建了一個數據表。另外還有executeInsert和executeUpdate方法用于執行插入和更新操作。由于Groovy支持多行字符串,所以我們不用像java那么費勁。
static void setUpTables(Sql sql) { 增刪查改插入數據可以使用execute或executeInsert方法。它們的主要區別是executeInsert方法會返回一個列表,包含了插入數據對應的所有主鍵。這兩個方法都支持?
占位符和額外的參數列表,這時候Groovy會使用PreparedStatement來執行SQL。它們也都支持GString和內插字符串。
下面的例子使用了占位符和參數列表執行插入操作。
static void insertRow(Sql sql) { println('插入數據') String stmt = 'insert into author(firstname,lastname) values(?,?)' def params = ['Yi', 'Tian'] def id = sql.executeInsert(stmt, params) assert id[0] == [0] params = ['Zhang', 'San'] id = sql.executeInsert(stmt, params) assert id[0] == [1] }如果喜歡傳統方式,可以使用query方法,會返回一個JDBC結果集可供查詢。
def expected = ['Dierk Koenig', 'Jon Skeet', 'Guillaume Laforge']def rowNum = 0sql.query('SELECT firstname, lastname FROM Author') { resultSet -> while (resultSet.next()) { def first = resultSet.getString(1) def last = resultSet.getString('lastname') assert expected[rowNum++] == "$first $last" }}Groovy也提供了幾個方便的方法來獲取數據。eachRow方法接受一個閉包參數,在閉包中,我們可以使用索引或成員訪問符來獲取每行的結果。
rowNum = 0sql.eachRow('SELECT firstname, lastname FROM Author') { row -> def first = row[0] def last = row.lastname assert expected[rowNum++] == "$first $last"}如果結果只有一行,還可以使用firstRow方法,它會返回GroovyRowResult對象。我們可以使用該對象提供的方法獲取數據。
def first = sql.firstRow('SELECT lastname, firstname FROM Author')Groovy還提供了rows方法,它會返回一個GroovyRowResult對象列表。
List authors = sql.rows('SELECT firstname, lastname FROM Author')assert authors.size() == 3更新數據也可以使用execute方法,或者使用executeUpdate方法。executeUpdate方法會返回受影響的行數。
static void updateAuthor(Sql sql) { def stmt = 'update author set firstname=?,lastname=? where id=?' def row = sql.executeUpdate(stmt, ['li', '4', 1]) assert row == 1 }刪除數據使用execute方法。
static void updateAuthor(Sql sql) { def stmt = 'update author set firstname=?,lastname=? where id=?' def row = sql.executeUpdate(stmt, ['li', '4', 1]) assert row == 1 }事務管理使用Sql的withTransaction方法,在閉包中執行的語句會自動包括在事務中。如果某個操作發生異常,整個事務就會回滾。下面的例子中,第二個SQL語句故意寫錯了,整個事務會回滾,我們可以看到事務前后的數據數量是相同的。
static void sqlTransaction(Sql sql) { println '事務管理' def rowsBefore = sql.firstRow('SELECT count(*) AS num FROM author').num try { sql.withTransaction { //正確語句 sql.executeInsert("INSERT INTO author(firstname,lastname) VALUES('wang','5')") sql.executeInsert("INSERT INTO author() VALUES(4324,3423)") } } catch (ignore) { println(ignore.message) } def rowsAfter = sql.firstRow('SELECT count(*) AS num FROM author').num assert rowsBefore == rowsAfter }Sql的withBatch方法提供了批處理功能。該方法的第一個參數是一次性批處理的數量。
sql.withBatch(3) { stmt -> stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Dierk', 'Koenig')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Paul', 'King')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Guillaume', 'Laforge')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Hamlet', 'D''Arcy')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Cedric', 'Champeau')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Erik', 'Pragt')" stmt.addBatch "INSERT INTO Author (firstname, lastname) VALUES ('Jon', 'Skeet')"}還可以使用預處理語句進行批處理。使用預處理的優點是執行語句的速度更快,缺點是所有批處理都是同一類型的語句。如果需要處理多個表,需要多個批處理語句。
def qry = 'INSERT INTO Author (firstname, lastname) VALUES (?,?)'sql.withBatch(3, qry) { ps -> ps.addBatch('Dierk', 'Koenig') ps.addBatch('Paul', 'King') ps.addBatch('Guillaume', 'Laforge') ps.addBatch('Hamlet', "D'Arcy") ps.addBatch('Cedric', 'Champeau') ps.addBatch('Erik', 'Pragt') ps.addBatch('Jon', 'Skeet')}Groovy提供了SQL數據庫的分頁功能。前面提到的很多方法都有分頁的版本,我們只要傳入起始索引和要獲取的數據量,即可得到相應的數據。
static void sqlPagination(Sql sql) { println('分頁') def stmt = 'select id,firstname,lastname from author' sql.rows(stmt, 1, 3).each { printRow(it) } println('----------------') sql.rows(stmt, 4, 3).each { printRow(it) } println('----------------') sql.rows(stmt, 7, 3).each { printRow(it) } println('----------------') }Groovy也能方便的獲取數據庫的元數據。要獲取數據庫元數據的話,調用Sql的connection.metaData
屬性即可。如果要獲取結果的元數據,最好的辦法就是定義一個元數據閉包,然后傳給相關方法,Groovy會保證元數據閉包只調用一次。當然也可以直接在結果閉包中調用結果的getMetaData()
方法獲取元數據,不過這樣這些代碼可能隨著結果的迭代重復執行多次。
SQL語句的參數不僅可以使用問號占位符,還可以使用命名參數和序號參數。命名參數很簡單,Hibernate等很多其他框架都支持。
sql.execute "INSERT INTO Author (firstname, lastname) VALUES (:first, :last)", first: 'Dierk', last: 'Koenig'還可以使用問號形式的命名參數,和上面等價。
sql.execute "INSERT INTO Author (firstname, lastname) VALUES (?.first, ?.last)", first: 'Jon', last: 'Skeet'如果傳遞的參數是Map或者對象形式的,還可以使用帶序號的命名參數形式。Groovy會自動解析合適的參數。
class Rockstar { String first, last }def pogo = new Rockstar(first: 'Paul', last: 'McCartney')def map = [lion: 'King']sql.execute "INSERT INTO Author (firstname, lastname) VALUES (?1.first, ?2.lion)", pogo, map新聞熱點
疑難解答