如何配置 Spring Boot 以使用两个数据库?

Posted

技术标签:

【中文标题】如何配置 Spring Boot 以使用两个数据库?【英文标题】:How to configure Spring boot for work with two databases? 【发布时间】:2018-08-07 13:42:03 【问题描述】:

我正在使用 Spring Boot 2.XHibernate 5 来连接不同服务器上的两个不同的 mysql 数据库(Bar 和 Foo)。我正在尝试从 REST 控制器中的方法列出实体的所有信息(自己的属性和 @OneToMany@ManyToOne 关系)。

我遵循了几个教程来执行此操作,因此,我能够获取我的 @Primary 数据库 (Foo) 的所有信息,但是,在检索 @ 时,我的辅助数据库 (Bar) 总是出现异常987654324@ 套。如果我将 @Primary 注释交换到 Bar 数据库,我可以从 Bar 数据库中获取数据,但不能从 Foo 数据库中获取数据。有没有办法解决这个问题?

这是我得到的例外:

...w.s.m.s.DefaultHandlerExceptionResolver :
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: 
    Could not write JSON document: failed to lazily initialize a collection of role: 
        com.foobar.bar.domain.Bar.manyBars, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]-com.foobar.bar.domain.Bar["manyBars"]); 
    nested exception is com.fasterxml.jackson.databind.JsonMappingException:
        failed to lazily initialize a collection of role: 
        com.foobar.bar.domain.Bar.manyBars, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->com.foobar.bar.domain.Bar["manyBars"])

我的 application.properties:

# MySQL DB - "foo"
spring.datasource.url=jdbc:mysql://XXX:3306/foo?currentSchema=public
spring.datasource.username=XXX
spring.datasource.password=XXX
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# MySQL DB - "bar"
bar.datasource.url=jdbc:mysql://YYYY:3306/bar?currentSchema=public
bar.datasource.username=YYYY
bar.datasource.password=YYYY
bar.datasource.driver-class-name=com.mysql.jdbc.Driver
# JPA
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

我的@PrimaryDataSource 配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        basePackages = "com.foobar.foo.repo")
public class FooDbConfig 

    @Primary
    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() 
        return DataSourceBuilder.create().build();
    

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) 
        return builder
                .dataSource(dataSource)
                .packages("com.foobar.foo.domain")
                .persistenceUnit("foo")
                .build();
    

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) 
        return new JpaTransactionManager(entityManagerFactory);
    

我的辅助数据源配置:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "barEntityManagerFactory",
        transactionManagerRef = "barTransactionManager", basePackages = "com.foobar.bar.repo")
public class BarDbConfig 

    @Bean(name = "barDataSource")
    @ConfigurationProperties(prefix = "bar.datasource")
    public DataSource dataSource() 
        return DataSourceBuilder.create().build();
    

    @Bean(name = "barEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean barEntityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier("barDataSource") DataSource dataSource) 
        return builder
                .dataSource(dataSource)
                .packages("com.foobar.bar.domain")
                .persistenceUnit("bar")
                .build();
    

    @Bean(name = "barTransactionManager")
    public PlatformTransactionManager barTransactionManager(
            @Qualifier("barEntityManagerFactory") EntityManagerFactory barEntityManagerFactory) 
        return new JpaTransactionManager(barEntityManagerFactory);
    

REST 控制器类:

@RestController
public class FooBarController 

    private final FooRepository fooRepo;
    private final BarRepository barRepo;

    @Autowired
    FooBarController(FooRepository fooRepo, BarRepository barRepo) 
        this.fooRepo = fooRepo;
        this.barRepo = barRepo;
    

    @RequestMapping("/foo")
    public List<Foo> listFoo() 
        return fooRepo.findAll();
    

    @RequestMapping("/bar")
    public List<Bar> listBar() 
        return barRepo.findAll();
    

    @RequestMapping("/foobar/id")
    public String fooBar(@PathVariable("id") Integer id) 
        Foo foo = fooRepo.findById(id);
        Bar bar = barRepo.findById(id);

        return foo.getName() + " " + bar.getName() + "!";
    


Foo/Bar 存储库:

@Repository
public interface FooRepository extends JpaRepository<Foo, Long> 
  Foo findById(Integer id);


@Repository
public interface BarRepository extends JpaRepository<Bar, Long> 
  Bar findById(Integer id);

@Primary 数据源的实体。第二个数据源的实体是一样的(只是改了类名):

@Entity
@Table(name = "foo")
public class Foo 

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @Column(name = "name")
    private String name;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "foo")
    @JsonIgnoreProperties("foo")
    private Set<ManyFoo> manyFoos = new HashSet<>(0);

    // Constructors, Getters, Setters


@Entity
@Table(name = "many_foo")
public class ManyFoo 

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @Column(name = "name")
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnoreProperties("manyFoos")
    private Foo foo;

    // Constructors, Getters, Setters
  

最后,我的应用程序 main:

@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        SpringApplication.run(Application.class, args);
    

值得注意的是,解决方案应为两个数据库保留 Lazy 属性以保持最佳性能。

编辑 1:如果两个目录(MySQL 术语中的“数据库”)在同一个数据库(“服务器”)中,Rick James 解决方案有效!!

问题仍然存在,当目录(MySQL 数据库)位于不同的数据库(服务器)中并试图保持 Lazy 属性时

非常感谢。

【问题讨论】:

哪个控制器方法抛出这个异常? "/foobar/id" ? @hovanessyan 从第二个数据库(Bar)中检索 OneToMany 对象的所有方法,例如“/bar”。根据您的第二个问题,方法“/foobar/id”不会抛出任何异常,因为它只返回对象 bar 的名称而不是 ManyToOne Foo foo。 您能否也发布您的存储库? 我编辑了问题以包含两个存储库。 【参考方案1】:

*ToMany 集合在 Hibernate 和 JPA 中默认是惰性的。该错误是因为 Jackson 试图在实体管理器(即休眠会话中的会话)关闭时序列化 OneToMany。因此,无法检索惰性集合。

默认情况下,带有 JPA 的 Spring Boot 为主 EM 提供 OpenEntityManagerInViewFilter。这允许只读数据库访问,但默认情况下仅适用于主 EM。

您有 3 个选项:

1) 您可以添加连接提取,例如How does the FetchMode work in Spring Data JPA

2) 您可以为非主要实体管理器添加一个 OpenEntityManagerInViewFilter 并将其添加到您的上下文中。

请注意,这意味着一个挑战,对于每个 Bar 和 Foo 实例,您的应用程序将返回数据库以检索 OneToMany。这部分不适用于 Bar,但适用于 Foo。这意味着一个可扩展性问题(某些人称为 N + 1 问题),因为对于每个 foo 和 bar,您运行一个额外的查询,这对于非平凡数量的 Foos 和 Bars 会变慢。

3) 另一种方法是让您在 Bar 和 Foo 上的收藏变得急切(参见 https://docs.oracle.com/javaee/7/api/javax/persistence/OneToMany.html#fetch--),但如果您完全关心可扩展性,则需要仔细分析。

我推荐选项 #1。

【讨论】:

open session in view 和 Eager fetch 是反模式。应该使用 JoinFetch。 不确定视图中的打开会话是一种反模式,它是默认的 Spring Boot 行为,尽管它为 N + 1 设置了一个。无论如何,添加了 join fetch,感谢您的输入。 可能感兴趣vladmihalcea.com/the-open-session-in-view-anti-pattern 非常感谢!!我知道渴望它是一种可能的灵魂,但仅适用于小型数据库。如果您有一个对象与一千个连接对象,这不是一个好主意。我尝试为两个数据库维护惰性属性。我试图找到一个类似于将两个数据库都作为主数据库的解决方案。我将编辑问题以添加此内容。 实际上,在大批量时,懒惰带来了不同的,有些人会说更糟糕的挑战。对于每个 Foo 或 Bar,您将运行一个对 db 的查询,一个较大的查询通常比许多较小的查询要好。【参考方案2】:

两个数据库(又名“目录”)在同一台服务器上?只使用一个连接。然后参考:

Foo.table1
Bar.table2

只要有一个简单的表名,就使用该语法。

不同的服务器

如果数据不在同一台机器上,它会变得混乱。几个想法:

从每个目录中获取数据,然后在应用程序代码中进行操作。该框架可能没有用于同时对两台服务器执行任何操作的挂钩。 使用 MariaDB 及其 FEDERATEDX 引擎。

【讨论】:

谢谢!如果两个数据库在同一台服务器上,这是一个很好的解决方案,但如果它们在不同的服务器上,则不起作用。 @Martin - 如果您的问题涉及 2 个不同的服务器(或单个服务器上的不同 MySQL 实例),请在您的问题中明确说明。您的代码听起来好像都在 localhost:3306 上连接,这必然是 same 服务器上的 same 实例。 我复制它以给出一个复制它的例子(我没有看到两个数据库在同一个网络中)。我编辑问题以明确这一点。但是对于将阅读您的问题的下一个用户,重要的是要说如果 @Table(name="Foo" ,catalog="FooDB") 和 @Table(name="Bar" ,catalog="BarDB") 您的示例有效它被添加。仅当它们位于相同的数据库中时。非常感谢! @Martin - 您的更改不明确,因此我进一步编辑了您的问题。 @rick-james,对于同一服务器中的两个不同目录,它可以工作,但有没有机会我可以避免 schema.table 名称而只写表名。

以上是关于如何配置 Spring Boot 以使用两个数据库?的主要内容,如果未能解决你的问题,请参考以下文章

如何配置两个实例mongodb使用spring boot和spring data

Spring Boot 配置和使用两个数据源

如何配置 IntelliJ HQL 控制台以使用 Spring Boot

启动时如何配置 Spring Boot 应用程序以在特定数据库上运行

如何在 Spring Boot 中模拟数据库连接以进行测试?

如何在Spring Boot和Spring Data中使用两个Cassandra数据源?