在Grails使用Sql獲取資料

jetgeng發表於2015-03-28

前因

Grails預設情況使用Hibernate作為資料存取的框架。不過Hibernate的缺點是眾所周知的。所以我們在一些複雜的場合需要通過 groovy.sql.Sql 直接使用sql來獲取資料。這樣就會存在如下的問題:

  1. 如何使用Grails配置的資料庫連線?
  2. 如何執行sql,進行資料庫相關操作?
  3. 如何將查詢的資料轉換成Domain Class?

下面就從上面這3個問題來說明如何在grails環境中直接使用sql來對資料庫進行操作。

連線資料庫

連線資料庫相對來說比較簡單,通過如下程式碼就可以完成。

class DoSomethingServices {
    def dataSource

    def queryDataWithSql(){
        Sql sql = new Sql(dataSource)
        sql.each("select * from sometable"){ it ->
            println it
        }
    }
}

程式碼詳細說明:

  • 第3行 注入dataSource, 這個dataSource 就是Grails中在DataSource.groovy中配置的 資料來源。 預設的hibernate也是在使用這個資料來源。
  • 第5 行 使用通過注入的dataSource物件 建立sql物件。 關於Sql物件的使用可以參考 http://groovy.codehaus.org/api/groovy/sql/Sql.html
  • 第6行 使用each方法執行一個sql語句。然後逐行回撥。

連線資料庫和查詢資料就這麼簡單。

到這裡肯定有人會問,如果需要往sql語句中加入引數怎麼辦。如何避免 Sql注入。 這個Sql在設計的過程中已經考慮到了。而且使用及其簡單。只要使用如下程式碼即可。

def queryDataWithSql(){
        Sql sql = new Sql(dataSource)
        def paramValue = ..
        def paramValue2 = ..
        sql.each("select * from sometable where field = ${paramValue} and field2 = ${paramValue2}"){ it ->
            println it
        }
    }

第5行直接使用GString傳入引數。最終執行的時候其實是講GString中得引數獲取出來。通過PreparedStatement傳入引數的方式。這樣可以避免sql的注入的攻擊。你不相信?那就看看Sql.java這個類中得eachRow方法吧。這個方法位於groovy-all-2.1.9-source.jar/groovy/sql/Sql.java 的第1236行。

資料如何轉換成Domain Class物件

這個問題是一個大問題。不過不是沒有辦法。最笨的辦法就是寫成如下的樣子:

sql.each("select field1 , field2 from sometable where field = ${paramValue} and field2 = ${paramValue2}"){ it ->
   def someDomain = new SomeDomin()
   someDomain.field1 = it["field1"]
   someDomain.field2 = it["field2"]
}

這個方法最大的缺點是程式碼量多,並且會有大量重複的程式碼。給人感覺很噁心。 在Groovy中又如下的辦法可以對物件的欄位賦值:

 def key = "field1"
 someDomain.getProperties()[key] = "someValue"

getProperties這個方法將該物件的所有值放到一個Map中返回。具體可參考http://groovy.codehaus.org/groovy-jdk/java/lang/Object.html#getProperties%28%29 對這個map進行賦值,就等於對這個物件進行賦值。 所以下面我只要有一個欄位和變數名對應的map,什麼就會搞定了。 於是有了如下的程式碼:

class DomainClassInfoService {

    def sessionFactory
    def grailsApplication

    def getDomainClass(clazzName) {
        return grailsApplication.domainClasses.find {
            it.name == clazzName
        }
    }

    def getFieldColumnMap(clazz) {
        def fieldColumnMap = [:]
        def hibernateMetaClass = sessionFactory.getClassMetadata(clazz)
        def grailsDomainClass = getDomainClass(clazz.getSimpleName())
        def domainProps = grailsDomainClass.getProperties()

        domainProps.each { prop ->
            //get the property's name
            def propName = prop.getName()
            //please refer to the hibernate javadoc
            //http://www.hibernate.org/hib_docs/v3/api/org/hibernate/persister/entity/AbstractEntityPersister.html
            def columnProps = hibernateMetaClass.getPropertyColumnNames(propName)
            if (columnProps && columnProps.length > 0) {
                //get the columnname, which is stored into the first array
                def columnName = columnProps[0]
                fieldColumnMap[propName] = columnName
            }
        }
        return fieldColumnMap
    }
}
以上程式碼說明如下:
  • 5 ~ 6 行注入將要使用的兩個服務,一個是hibernate的sessionFactory, 另外一個是grailsApplication 上下文
  • 7 ~ 9 這個方法是根據給定的段類名。比如有一個Domain Class的全名為 org.gunn.domain.Book 這裡的clazzName 就是Book。 * 第 8 行是從grailsApplication中獲取所有Domain Class的DefaultGrailsDomainClass這個類的物件。這裡牽涉到一個Artefact的概念,請參考 https://grails.org/Developer+-+Artefact+API
  • 12 ~ 28 行就是 根據Domain Class中的變數來獲取資料庫對應的的欄位名。 有程式碼在這裡就不多解釋了。

結合我們上面的那個properties的小技巧,我們就使用如下程式碼來完成使用Sql查詢資料,轉換成Domain Class的物件。

String querySql = ''' select * from table where field1 = ? '''

def tripSegmentFieldColumnMap = domainClassInfoService.getFieldColumnMap(SomeDomain)
Sql sql = new Sql(dataSource)
sql.eachRow(querySql, field1Value){
   SomeDomain someObject = new SomeDomain()
   tripSegmentFieldColumnMap.each { key, value ->
        someObject.getProperties()[key] = it[value]
   }
 }

這個方法對於非關係的,沒有太大問題。如果有類似於一對多這樣的關係的話,會引起hibernate中著名的n+1的問題。例如SomeDomain 中有一個變數是SomeParent, 並且SomeDomain belong to 這個SomeParent的話。那麼像上面那樣直接賦值就會引起去發起資料庫查詢請求查詢SomeParent的。所以可以使用如下的方式進行避免:

sql.eachRow(querySql, field1Value){
   SomeDomain someObject = new SomeDomain()
   SomeParent someParent = new SomeParent()
   someParent.id = it.parentId
   tripSegmentFieldColumnMap.each { key, value ->
        if(key != "parentId")
            someObject.getProperties()[key] = it[value]
   }
   someObject.parent = someParent

這個辦法很土,如果你有更好的。歡迎分享!謝謝!

相關文章