Как настроить загрузку 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.