Как настроить загрузку Spring для работы с двумя базами данных?

Я использую Spring Boot 2.X с Hibernate 5 для подключения двух разных баз данных MySQL (Bar и Foo) на разных серверах. Я пытаюсь перечислить всю информацию об объекте (собственные атрибуты и отношения @OneToMany и @ManyToOne) из метода в REST Controller.

Я выполнил несколько руководств, чтобы сделать это, поэтому я могу получить всю информацию для моей базы данных @Primary (Foo), однако всегда получаю исключение для моей вторичной базы данных (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"])

Мои приложения.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 источника данных @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 свойство

Большое спасибо.

Ответы

Ответ 1

* Коллекции ToMany полны по умолчанию в Hibernate & JPA. Ошибка заключается в том, что Джексон пытается сериализовать OneToMany, когда диспетчер сущностей (ака сеанс в режиме гибернации) закрыт. Следовательно, ленивые коллекции не могут быть получены.

Spring Boot с JPA по умолчанию предоставляет OpenEntityManagerInViewFilter для первичной EM. Это позволяет использовать доступ только для чтения DB, но по умолчанию работает только для первичной EM.

У вас есть 3 варианта:

1) Вы можете добавить выбор подключения, например, как работает FetchMode в Spring Data JPA

2) Вы можете добавить OpenEntityManagerInViewFilter для не первичного менеджера сущностей и добавить его в свой контекст.

Обратите внимание: это означает, что для каждого экземпляра Bar и Foo ваше приложение вернется в базу данных для извлечения OneToMany. Это та часть, которая не работает в баре, но для Foo. Это подразумевает проблему масштабируемости (например, проблему N + 1), так как для каждого foo и bar вы запускаете дополнительный запрос, который будет медленным для нетривиальных сумм Foos и Bars.

3) Альтернативой является создание вашей коллекции в Bar and Foo eager (см. Этот https://docs.oracle.com/javaee/7/api/javax/persistence/OneToMany.html#fetch--), но это должно быть тщательно анализируется, если для вас важна масштабируемость.

Я бы порекомендовал вариант №1.

Ответ 2

Две базы данных (иначе называемые "каталоги") на одном сервере? Используйте только одно соединение. Затем сделайте следующее:

Foo.table1
Bar.table2

Используйте этот синтаксис везде, где у вас будет простое имя таблицы.

Различные серверы

Это становится беспорядочным, если данные не находятся на одной машине. Несколько идей:

  • Извлеките данные из каждого каталога, а затем обработайте их в коде приложения. У рамки, вероятно, нет крючков для того, чтобы делать что-либо на обоих серверах одновременно.
  • Используйте MariaDB и его FEDERATEDX Engine.