Весна — абстрактный источник данных маршрутизации

Опубликовано: 22 Февраля, 2023

В этой статье мы рассмотрим Spring AbstractRoutingDatasource как абстрактную реализацию DataSource, которая динамически определяет фактический DataSource на основе текущего контекста. Теперь этот вопрос придет нам в голову, когда он нам может понадобиться, т.е. когда мы должны перейти к реализации AbstractRoutingDatasource. Иногда у нас есть требование динамически переключать базы данных на основе филиала/региона/локали или любого другого параметра и выполнять операцию на основе запроса. Итак, давайте разберемся с реализацией AbstractRoutingDatasource на примере. Предположим, у нас есть два филиала GeeksForGeeks, то есть Noida и Bangalore, и по одной базе данных для каждого филиала. Нам нужно получить детали из базы данных Noida, если запрос поступает из филиала Noida, а если запрос поступает из филиала Bangalore, то из базы данных Bangalore.

В этом руководстве мы предоставим конечную точку REST, которая подключается либо к noidadb, либо к bangaloredb, и будет извлекать информацию о стажерах из таблицы на основе запроса и возвращать объекты JSON. Здесь мы создадим конечную точку: http://localhost:8080/interns. и мы реализуем AbstractRoutingDatasource в приложении. Когда мы доберемся до конечной точки http://localhost:8080/interns из филиала Bangalore, мы получим приведенный ниже ответ, который представляет собой не что иное, как записи из базы данных bangaloredb.

[
   {
       "id": 100,
       "name": "Bishal Dubey",
       "address": "Dibrugarh, Assam",
       "dob": "11-11-1994",
       "branch": "bangalore"
   },
   {
       "id": 101,
       "name": "Bikash Dubey",
       "address": "Dibrugarh, Assam",
       "dob": "07-03-1997",
       "branch": "bangalore"
   },
   {
       "id": 102,
       "name": "Nisha Ojha",
       "address": "Makum, Assam",
       "dob": "31-08-1997",
       "branch": "bangalore"
   }
]

Когда мы доберемся до конечной точки http://localhost:8080/interns из ветки Noida, мы получим приведенный ниже ответ, который представляет собой не что иное, как записи из базы данных noidadb.

[
   {
       "id": 1,
       "name": "Neha Dubey",
       "address": "Doomdooma, Assam",
       "dob": "31-08-1992",
       "branch": "noida"
   },
   {
       "id": 2,
       "name": "Roshan Ojha",
       "address": "Makum, Assam",
       "dob": "22-10-1998",
       "branch": "noida"
   },
   {
       "id": 3,
       "name": "Muskan Pandey",
       "address": "Margherita, Assam",
       "dob": "12-05-2000",
       "branch": "noida"
   }
]

Note: Whenever we will hit the endpoint then we will pass one extra parameter in the request header named “branch” with the value Noida/Bangalore.

Теперь давайте перейдем к реализации AbstractRoutingDatasource, мы будем действовать шаг за шагом.

Пошаговая реализация

Структура проекта:

Создайте проект maven с необходимыми зависимостями в файле pom.xml .

XML




<?xml version="1.0" encoding="UTF-8"?>
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath />
    </parent>
    <groupId>com.gfg.scripter</groupId>
    <artifactId>scripter-datasource-routing</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
  
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
  
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Настройте источники базы данных в application.properties как для noidadb, так и для bangaloredb с несколькими другими конфигурациями.

# database details for noida branch
noida.datasource.url=jdbc:mysql://localhost:3306/noidadb?createDatabaseIfNotExist=true
noida.datasource.username=root
noida.datasource.password=root

# database details for bangalore branch
bangalore.datasource.url=jdbc:mysql://localhost:3306/bangaloredb?createDatabaseIfNotExist=true
bangalore.datasource.username=root
bangalore.datasource.password=root

# JPA property settings
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true

Создайте сущность стажера , которая будет использоваться для ссылки на стажера в обеих базах данных.

Java




package com.gfg.scripter.entity;
  
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
  
@Entity
@Table(name = "intern")
public class Intern {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String address;
    private String dob;
    private String branch;
    // Generate Getters and Setters
}

Создайте контекст источника данных , т.е. это не что иное, как перечисление/константа, которая содержит имя обеих ветвей gfg. Здесь AbstractRoutingDataSource нужна часть информации, чтобы решить, к какой базе данных направить маршрут, и здесь этот контекст источника данных поможет.

Java




package com.gfg.scripter.constant;
  
public enum BranchEnum {
    NOIDA, BANGALORE
}

Теперь мы создадим Context Holder , который будет содержать текущий контекст как ссылку ThreadLocal.

Java




package com.gfg.scripter.config;
  
import com.gfg.scripter.constant.BranchEnum;
  
public class BranchContextHolder {
  
    private static ThreadLocal<BranchEnum> threadLocal = new ThreadLocal<>();
  
    public static void setBranchContext(BranchEnum branchEnum) {
        threadLocal.set(branchEnum);
    }
  
    public static BranchEnum getBranchContext() {
        return threadLocal.get();
    }
  
    public static void clearBranchContext() {
        threadLocal.remove();
    }
}

Создайте класс DataSource Routing , который расширит класс AbstractRoutingDatasource и будет содержать карту реальных источников данных.

Java




package com.gfg.scripter.config;
  
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import com.gfg.scripter.constant.BranchEnum;
  
public class DataSourceRouting extends AbstractRoutingDataSource {
  
    @Override
    protected Object determineCurrentLookupKey() {
        return BranchContextHolder.getBranchContext();
    }
  
    public void initDatasource(DataSource bangaloreDataSource,
            DataSource noidaDataSource) {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(BranchEnum.NOIDA, noidaDataSource);
        dataSourceMap.put(BranchEnum.BANGALORE, bangaloreDataSource);
        this.setTargetDataSources(dataSourceMap);
          
          // Here we have to provide default datasource details.
        this.setDefaultTargetDataSource(noidaDataSource);
    }
}

Теперь мы создадим конфигурацию DataSource , т.е. объект/компонент баз данных noida/bangalore и другие необходимые конфигурации, т.е. диспетчер транзакций и диспетчер сущностей, которые будут указывать на сущность Intern и репозиторий Intern.

Java




package com.gfg.scripter.config;
  
import javax.sql.DataSource;
  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
  
import com.gfg.scripter.entity.Intern;
  
@Configuration
@EnableJpaRepositories(basePackages = "com.gfg.scripter.repository", transactionManagerRef = "transcationManager", entityManagerFactoryRef = "entityManager")
@EnableTransactionManagement
public class DataSourceConfig {
  
    @Bean
    @Primary
    @Autowired
    public DataSource dataSource() {
        DataSourceRouting routingDataSource = new DataSourceRouting();
        routingDataSource.initDatasource(noidaDataSource(), bangaloreDataSource());
        return routingDataSource;
    }
  
    @Bean
    @ConfigurationProperties(prefix = "noida.datasource")
    public DataSource noidaDataSource() {
        return DataSourceBuilder.create().build();
    }
  
    @Bean
    @ConfigurationProperties(prefix = "bangalore.datasource")
    public DataSource bangaloreDataSource() {
        return DataSourceBuilder.create().build();
    }
  
    @Bean(name = "entityManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dataSource()).packages(Intern.class).build();
    }
  
    @Bean(name = "transcationManager")
    public JpaTransactionManager transactionManager(
            @Autowired @Qualifier("entityManager") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        return new JpaTransactionManager(entityManagerFactoryBean.getObject());
    }
}

Теперь мы создадим еще один класс, т. е. DataSource Interceptor , который будет перехватывать каждый запрос и извлекать информацию о ветке из заголовка запроса и помещать ее в держатель контекста, который мы уже создали, т. е. BranchContextHolder, который будет переключать источник данных. После этого зарегистрируем этот перехватчик в WebMvcConfigurer.

Java




package com.gfg.scripter.config;
  
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.gfg.scripter.constant.BranchEnum;
  
@Component
public class DataSourceInterceptor extends HandlerInterceptorAdapter {
  
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {
  
        String branch = request.getHeader("branch");
        if (BranchEnum.BANGALORE.toString().equalsIgnoreCase(branch))
            BranchContextHolder.setBranchContext(BranchEnum.BANGALORE);
        else
            BranchContextHolder.setBranchContext(BranchEnum.NOIDA);
        return super.preHandle(request, response, handler);
    }
}

Java




package com.gfg.scripter.config;
  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  
@Configuration
public class WebConfig implements WebMvcConfigurer {
  
    @Autowired
    private DataSourceInterceptor dataSourceInterceptor;
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {