显示导航

Grails多数据源

了解如何使用 Grails 应用程序消费和处理对多个数据源的事务。

作者:Sergio del Amo

Grails版本 3.3.1

1 培训

Grails培训 - 由创建并积极维护Grails框架的工作人员制定并提供!

2 入门

在本指南中,您将…

2.1 所需内容

要完成本指南,您需要具备以下条件

  • 有一些时间

  • 一个不错的文本编辑器或 IDE

  • JDK 1.8 或更高版本已安装,并正确配置了 JAVA_HOME

2.2 完成指南的方法

要开始,请执行以下操作

或者

Grails 指南仓库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中含有一些额外代码,以便于您开始。

  • complete 一个已完成的示例。这是完成指南所介绍的步骤并在 initial 文件夹中应用了这些更改后的结果。

若要完成指南,请转到 initial 文件夹

  • cd 输入到 grails-guides/grails-multi-datasource/initial

并按照下一节中的说明进行操作。

如果您将 cd 输入到 grails-guides/grails-multi-datasource/complete 中,则可以直接到已完成的示例

3 编写应用程序

我们正在使用 rest-profile 编写一个 Grails 应用程序,该应用程序连接到两个数据源。

graph
在以前版本中,Grails 为多种数据源使用最尽力事务链来尝试管理所有已配置数据源间的事务。从 Grails 3.3 开始,禁用此功能,因为它造成了困惑,因为这不是真正的 XA 实现,而且对于每个事务,还影响其性能,无论实际的要求是什么,它都有一个为每个数据源绑定的事务。

3.1 配置

application.yml 中绑定两个数据源

grails-app/conf/application.yml
dataSource:
    pooled: true
    jmxExport: true
    driverClassName: org.h2.Driver
    username: sa
    password: ''
environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        dataSources:
            books:
                dbCreate: create-drop
                url: jdbc:h2:mem:bookDevDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    test:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        dataSources:
            books:
                dbCreate: create-drop
                url: jdbc:h2:mem:bookTestDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE

3.2 域类

创建 Movie 域类。如果我们不指定任何数据源,它将关联到default数据源。

grails-app/domain/demo/Movie.groovy
package demo

class Movie {
    String title
    static hasMany = [keywords: Keyword]
}

创建 Book 域类。

grails-app/domain/demo/Book.groovy
package demo

class Book {
    String title

    static hasMany = [keywords: Keyword]

    static mapping = {
        datasource 'books' (1)
    }
}
1 Book 域类关联到 books 数据源。

创建 Keyword 域类。

grails-app/domain/demo/Keyword.groovy
package demo

import org.grails.datastore.mapping.core.connections.ConnectionSource

class Keyword {
    String name

    static mapping = {
        datasources([ConnectionSource.DEFAULT, 'books']) (1)
    }
}
1 Keyword 域类关联到两个数据源(dataSource - 默认数据源 - 和 books)。

3.3 服务

Movie 域类创建 DataService

grails-app/services/demo/MovieDataService.groovy
package demo

import grails.gorm.services.Join
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Movie)
interface MovieDataService {

    void deleteByTitle(String title)

    @Join('keywords') (1)
    List<Movie> findAll()
}
1 您可以使用 @Join 注解指定查询联接。
如果您使用 @ReadOnly 代替 @ReadOnly('books'),您将收到如下异常:org.hibernate.HibernateException: No Session found for current thread

添加常规服务

grails-app/services/demo/MovieService.groovy
package demo

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class MovieService {

    @Transactional
    Movie addMovie(String title, List<String> keywords) {
        Movie movie = new Movie(title: title)
        if ( keywords ) {
            for ( String keyword : keywords ) {
                movie.addToKeywords(new Keyword(name: keyword))
            }
        }
        if ( !movie.save() ) {
            log.error 'Unable to save movie'
        }
        movie
    }



}
grails-app/services/demo/BookDataService.groovy
package demo

import grails.gorm.services.Join
import grails.gorm.services.Service
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Service(Book)
interface BookDataService {

    @Join('keywords') (2)
    @ReadOnly('books') (1)
    List<Book> findAll()

    @Transactional('books') (1)
    void deleteByTitle(String title)
}
1 @Transactional@ReadOnly 注解中指定数据源名称。
2 您可以使用 @Join 注解指定查询联接。

添加常规服务

grails-app/services/demo/BookService.groovy
package demo

import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
class BookService {

    @Transactional('books') (1)
    Book addBook(String title, List<String> keywords) {
        Book book = new Book(title: title)
        if ( keywords ) {
            for ( String keyword : keywords ) {
                book.addToKeywords(new Keyword(name: keyword))
            }
        }
        if ( !book.save() ) {
            log.error 'Unable to save book'
        }
        book
    }
}
1 @Transactional 注解中指定数据源名称。

添加一个使用多个数据源的 Keyword 服务。

在域类中指定的首个数据源是未使用显式命名空间时的默认数据源。对于 Keyword,默认使用第一个指定的数据源 ConnectionSource.DEFAULT

grails-app/services/demo/KeywordService.groovy
package demo

import grails.gorm.DetachedCriteria
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileStatic

@CompileStatic
class KeywordService {

    @ReadOnly('books')
    List<Keyword> findAllBooksKeywords() {
        booksQuery().list()
    }

    @ReadOnly
    List<Keyword> findAllDefaultDataSourceKeywords() {
        defaultDataSourceQuery().list()
    }

    private DetachedCriteria<Keyword> booksQuery() {
        Keyword.where {}.withConnection('books') (1)
    }

    private DetachedCriteria<Keyword> defaultDataSourceQuery() {
        Keyword.where {}
    }
}
1 为查询使用 withConnection 指定数据源名称。

您本来可以使用动态查找器或条件查询而不是where 查询编写 books 查询。

where 查询:Keyword.where {}.withConnection('books').list()

动态查找器:Keyword.books.findAll()

条件:Keyword.books.createCriteria().list { }

3.4 控制器

创建 BookControllerMovieController。这些控制器使用以前实现的服务。

grails-app/controllers/demo/SaveBookCommand.groovy
package demo

import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable

@GrailsCompileStatic
class SaveBookCommand implements Validateable {
    String title
    List<String> keywords

    static constraints = {
        title nullable: false
        keywords nullable: true
    }
}
grails-app/controllers/demo/BookController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BookController {

    static allowedMethods = [save: 'POST', index: 'GET']

    static responseFormats = ['json']

    BookService bookService

    KeywordService keywordService

    BookDataService bookDataService

    def save(SaveBookCommand cmd) {
        bookService.addBook(cmd.title, cmd.keywords)
        render status: 201
    }

    def index() {
        [bookList: bookDataService.findAll()]
    }

    def delete(String title) {
        bookDataService.deleteByTitle(title)
        render status: 204
    }

    def keywords() {
        render view: '/keyword/index',
               model: [keywordList: keywordService.findAllBooksKeywords()]
    }
}
grails-app/controllers/demo/SaveBookCommand.groovy
package demo

import grails.compiler.GrailsCompileStatic
import grails.validation.Validateable

@GrailsCompileStatic
class SaveBookCommand implements Validateable {
    String title
    List<String> keywords

    static constraints = {
        title nullable: false
        keywords nullable: true
    }
}
grails-app/controllers/demo/MovieController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class MovieController {

    static allowedMethods = [save: 'POST', index: 'GET']

    static responseFormats = ['json']

    MovieService movieService

    MovieDataService movieDataService

    KeywordService keywordService

    def save(SaveMovieCommand cmd) {
        movieService.addMovie(cmd.title, cmd.keywords)
        render status: 201
    }

    def index() {
        [movieList: movieDataService.findAll()]
    }

    def delete(String title) {
        movieDataService.deleteByTitle(title)
        render status: 204
    }

    def keywords() {
        render view: '/keyword/index',
               model: [keywordList: keywordService.findAllDefaultDataSourceKeywords()]
    }
}

`

3.5 视图

添加多个 JSON 视图 来呈现输出。

grails-app/views/book/_book.gson
import demo.Book

model {
    Book book
}

json {
    title book.title
    keywords book.keywords*.name
}
grails-app/views/book/index.gson
import demo.Book

model {
    Iterable<Book> bookList
}

json tmpl.book(bookList)
grails-app/views/movie/_movie.gson
import demo.Movie

model {
    Movie movie
}

json {
    title movie.title
    keywords movie.keywords*.name
}
grails-app/views/movie/index.gson
import demo.Movie

model {
    Iterable<Movie> movieList
}

json tmpl.movie(movieList)
grails-app/views/keyword/index.gson
import demo.Keyword

model {
    Iterable<Keyword> keywordList
}

json {
    keywords keywordList*.name.unique().sort()
}

3.6 功能测试

添加 grails-datastore-rest-client 作为 testCompile 依赖关系

build.gradle
testCompile "org.grails:grails-datastore-rest-client"

添加一个功能测试,该测试验证 Grails 是否按预期处理了多个数据源

src/integration-test/groovy/demo/MultipleDataSourceSpec.groovy
package demo

import grails.plugins.rest.client.RestBuilder
import grails.plugins.rest.client.RestResponse
import grails.testing.mixin.integration.Integration
import spock.lang.Shared
import spock.lang.Specification

@Integration
class MultipleDataSourceSpec extends Specification {

    @Shared
    RestBuilder rest = new RestBuilder()

    private RestResponse saveResource(String resource, String itemTitle, List<String> itemKeywords) {
        rest.post("http://localhost:${serverPort}/${resource}") {
            accept('application/json')
            contentType('application/json')
            json {
                title = itemTitle
                keywords = itemKeywords
            }
        }
    }

    private RestResponse deleteResource(String resource, String itemTitle) {
        rest.delete("http://localhost:${serverPort}/${resource}") {
            accept('application/json')
            contentType('application/json')
            json {
                title = itemTitle
            }
        }
    }

    private RestResponse fetchResource(String resource) {
        rest.get("http://localhost:${serverPort}/${resource}") {
            accept('application/json')
        }
    }

    private RestResponse resourceKeywords(String resource) {
        rest.get("http://localhost:${serverPort}/${resource}/keywords") {
            accept('application/json')
        }
    }

    def "Test Multi-Datasource support saving and retrieving books and movies"() {
        when:
        RestResponse resp = saveResource('book', 'Change Agent', ['dna', 'sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('book', 'Influx', ['sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('book', 'Kill Decision', ['drone', 'sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('book', 'Freedom (TM)', ['sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('book', 'Daemon', ['sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('movie', 'Pirates of Silicon Valley', ['apple', 'microsoft', 'technology'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = saveResource('movie', 'Inception', ['sci-fi'])

        then:
        resp.statusCode.value() == 201

        when:
        resp = fetchResource('book')

        then:
        resp.statusCode.value() == 200
        resp.json.collect { it.title }.sort() == ['Change Agent', 'Daemon', 'Freedom (TM)', 'Influx', 'Kill Decision']

        when:
        resp = fetchResource('movie')

        then:
        resp.statusCode.value() == 200
        resp.json.collect { it.title }.sort() == ['Inception', 'Pirates of Silicon Valley']

        when:
        resp = resourceKeywords('book')

        then:
        resp.statusCode.value() == 200
        resp.json.keywords == ['dna', 'drone', 'sci-fi']

        when:
        resp = resourceKeywords('movie')

        then:
        resp.statusCode.value() == 200
        resp.json.keywords == ['apple', 'microsoft', 'sci-fi', 'technology']


        when:
        resp = deleteResource('book', 'Change Agent')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('book', 'Influx')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('book', 'Kill Decision')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('book', 'Freedom (TM)')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('book', 'Daemon')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('movie', 'Pirates of Silicon Valley')

        then:
        resp.statusCode.value() == 204

        when:
        resp = deleteResource('movie', 'Inception')

        then:
        resp.statusCode.value() == 204
    }
}

4 测试应用程序

运行测试

./grailsw
grails> test-app
grails> open test-report

或者

./gradlew check
open build/reports/tests/index.html

5 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的创建。我们可以提供各种咨询服务和支持服务。

OCI 是 Grails 的家

认识团队