问题 如何配置Spring启动以使用两个数据库?


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

我已经按照几个教程来做到这一点,因此,我能够获得我的所有信息 @Primary 但是,在检索时,我总是得到我的辅助数据库(Bar)的异常 @OneToMany 集。如果我交换了 @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

我的 @Primary DataSource配置:

@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);
    }
}

我的辅助DataSource配置:

@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
}  

最后,我的应用程序主要:

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

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

编辑1: 如果两个目录(MySQL术语中的“数据库”)都在同一个数据库(“服务器”)中,那么Rick James解决方案就可以工作!!

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

非常感谢。


5301
2018-02-27 19:38


起源

哪个控制器方法抛出此异常? “/ foobar / {id}”? - hovanessyan
@hovanessyan所有方法从第二个数据库(Bar)检索OneToMany对象,例如“/ bar”。根据你的第二个问题,方法“/ foobar / {id}”不会抛出任何异常,因为它只返回对象栏的名称而不是ManyToOne Foo foo。 - Martin
您还可以发布您的存储库吗? - hovanessyan
我编辑了这个问题以包含两个存储库。 - Martin


答案:


*默认情况下,ToMany Collections在Hibernate和JPA中是惰性的。错误是因为当实体管理器(也就是hibernate-speak中的会话)关闭时,Jackson正在尝试序列化OneToMany。因此,无法检索惰性集合。

默认情况下,Spring Boot with JPA提供了一个 OpenEntityManagerInViewFilter 对于初级EM。这允许只读数据库访问,但默认情况下仅适用于主EM。

你有3个选择:

1)您可以添加联接提取,例如 FetchMode如何在Spring Data JPA中工作

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

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

3)另一种方法是让你的收藏品在Bar and Foo渴望(见这个 https://docs.oracle.com/javaee/7/api/javax/persistence/OneToMany.html#fetch-- )但是,如果可伸缩性是您的关注点,则需要仔细分析。

我建议选项#1。


9
2018-03-07 18:13



在视图中打开会话和Eager fetch是反模式。应该使用JoinFetch。 - hovanessyan
不确定在视图中打开会话是一种反模式,它是默认的弹簧启动行为,虽然它为N + 1设置了一个。无论如何,添加了连接提取,感谢输入。 - Taylor
可能有趣 vladmihalcea.com/the-open-session-in-view-anti-pattern - hovanessyan
非常感谢!!我知道它渴望有一个可靠的灵魂,但只适用于微小的数据库。如果你有一个带有一个mille连接对象的对象,这不是一个好主意。我试着保留两个女孩的懒惰财产。我试图找到类似于将两个数据库作为主数据库的解决方案。我要编辑问题来添加它。 - Martin
实际上,在大量,懒惰呈现出不同的,有些人会说更糟糕的挑战。对于每个Foo或Bar,您将对db运行查询,一个更大的查询通常比许多更小的查询更好。 - Taylor


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

Foo.table1
Bar.table2

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

不同的服务器

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

  • 从每个目录中获取数据,然后在应用程序代码中进行操作。该框架可能没有钩子同时对两个服务器做任何事情。
  • 使用MariaDB及其 FEDERATEDX 发动机。

2
2018-03-07 19:16



谢谢!如果两个数据库位于同一服务器中,则它是一个很好的解决方案,但如果它们位于不同的服 - Martin
@Martin - 如果您的问题涉及2个不同的服务器(或单个服务器上的不同MySQL实例),那么您的问题中的内容就会明确。你的代码听起来像都在localhost上连接:3306,这必然是 相同 实例 相同 服务器。 - Rick James
我复制它给一个例子来复制它(我看不到两个数据库在同一个网络中)。我编辑问题以明确这一点。但是对于将要阅读您的问题的下一个用户,重要的是说如果@Table(name =“Foo”,catalog =“FooDB”)和@Table(name =“Bar”,catalog =“BarDB”)您的示例有效它被添加。只有它们在同一个数据库中。非常感谢! - Martin
@Martin - 你的更改含糊不清,所以我进一步编辑了你的问题。 - Rick James