显示导航

Grails 多数据源

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

作者:塞尔吉奥·德尔阿莫

Grails 版本 4.0.1

1 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!。

2 入门

在本文档中,你将...​

2.1 所需内容

要完成本文档,你需要以下内容

  • 一些时间

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

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

2.2 如何完成指南

要开始,请执行以下操作

Grails 指南存储库包含两个文件夹

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

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

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

  • cdgrails-guides/grails-multi-datasource/initial

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

如果你 cdgrails-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 注解指定查询联接。

添加常规服务

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 注解指定查询联接。
如果您使用 @ReadOnly 代替 @ReadOnly('books'),则您将获得异常:org.hibernate.HibernateException: 未针对当前线程发现会话

添加常规服务

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 grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
@Transactional
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.unique().sort()
}
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.unique().sort()
}
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 功能测试

将 *micronaut-http-client* 作为 *testCompile* 依赖项添加

build.gradle
testCompile "io.micronaut:micronaut-http-client"

添加一个验证 Grails 预期处理多个数据源的功能测试

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

import grails.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import spock.lang.Shared
import spock.lang.Specification

@Integration
class MultipleDataSourceSpec extends Specification {

    @Shared
    HttpClient client

    @OnceBefore
    void init() {
        String baseUrl = "http://localhost:$serverPort"
        this.client  = HttpClient.create(baseUrl.toURL())
    }

    private HttpResponse<Map> saveResource(String resource, String itemTitle, List<String> itemKeywords) {
        HttpRequest request = HttpRequest.POST("/$resource", [title: itemTitle, keywords: itemKeywords])
        client.toBlocking().exchange(request, Map)
    }

    private HttpResponse deleteResource(String resource, String itemTitle) {
        HttpRequest request = HttpRequest.DELETE("/$resource", [title: itemTitle])
        client.toBlocking().exchange(request)
    }

    private HttpResponse<List<Map>> fetchResource(String resource) {
        HttpRequest request = HttpRequest.GET("/$resource")
        client.toBlocking().exchange(request, Argument.of(List, Map))
    }

    private HttpResponse<Map> resourceKeywords(String resource) {
        HttpRequest request = HttpRequest.GET("/$resource/keywords")
        client.toBlocking().exchange(request, Map)
    }

    def "Test Multi-Datasource support saving and retrieving books and movies"() {
        given:
        List<Map> books = [
                [title: 'Change Agent', tags: ['dna', 'sci-fi']],
                [title: 'Influx', tags: ['sci-fi']],
                [title: 'Kill Decision', tags: ['drone', 'sci-fi']],
                [title: 'Freedom (TM)', tags: ['sci-fi']],
                [title: 'Daemon', tags: ['sci-fi']],
        ]
        List<Map> movies = [
                [title: 'Pirates of Silicon Valley', tags: ['apple', 'microsoft', 'technology']],
                [title: 'Inception', tags: ['sci-fi']],
        ]
        books.each { book ->
            HttpResponse<Map> resp = saveResource('book', book.title as String, book.tags as List<String>)
            assert resp.status == HttpStatus.CREATED
        }
        movies.each { movie ->
            HttpResponse<Map> resp = saveResource('movie', movie.title as String, movie.tags as List<String>)
            assert resp.status == HttpStatus.CREATED
        }

        when:
        HttpResponse<List<Map>> resourceResp = fetchResource('book')

        then:
        resourceResp.status == HttpStatus.OK
        resourceResp.body().collect { it.title }.sort() == books.collect { it.title }.sort()

        when:
        resourceResp = fetchResource('movie')

        then:
        resourceResp.status == HttpStatus.OK
        resourceResp.body().collect { it.title }.sort() == movies.collect { it.title }.sort()

        when:
        HttpResponse<Map> resp = resourceKeywords('book')

        then:
        resp.status == HttpStatus.OK
        (resp.body().keywords as List<String>).sort() == books.collect { it.tags }.flatten().unique().sort()

        when:
        resp = resourceKeywords('movie')

        then:
        resp.status == HttpStatus.OK
        (resp.body().keywords as List<String>).sort() == movies.collect { it.tags }.flatten().unique().sort()

        cleanup:
        books.each { book ->
            assert deleteResource('book', book.title as String).status() == HttpStatus.NO_CONTENT
        }
        movies.each { movie ->
            assert deleteResource('movie', movie.title as String).status() == HttpStatus.NO_CONTENT
        }
    }
}

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 的家

认识团队