Маршрутизация базы данных с использованием MultiTenancy весной

Я пытаюсь реализовать мультиарендность весной. Я использую JWT, и у меня есть tenant_id, хранящийся в JWT. Я настроил свой класс MultiTenantDataSourceRouter, расширяющий AbstractRoutingDataSource, чтобы ключ поиска был tenant_id.

@Override
protected Object determineCurrentLookupKey() {
    var authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication!= null && authentication.getPrincipal() instanceof User user) {
        return user.getTenant_id();
    }
    return null;
}

У меня также есть метод контроллера, который входит в систему пользователя. При входе в систему я хочу создать объект пользователя и сохранить его в базе данных, которая, как предполагается, принадлежит этому арендатору.

public ResponseEntity<AuthenticationResponse> loginUser(AuthenticateRequest authenticateRequest) {
            //authenticate the User
            authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(authenticateRequest.getEmail(),authenticateRequest.getPassword()));

            //this line of code runs oly if authentication is successful
            User user = userRepository.findByEmail(authenticateRequest.getEmail()).orElseThrow();
            String jwt = jwtService.generateToken(user);

            //set the security context to allow routing of db and to know the tenantID and also set the datasource
            SecurityContextHolder.getContext()
                    .setAuthentication(new UsernamePasswordAuthenticationToken(user.getUsername(),null,user.getAuthorities()));

            var mds = new MultiTenantDataSourceRouter();
            mds.setTargetDataSources(dataSourceConfig.loadAllDataSources());
            mds.afterPropertiesSet();

            //create an Employee Object for the user once
            Employee employee = employeeRepository.findByEmail(user.getEmail());
            if (employee!= null){
                Employee newEmployee = Employee.builder()
                        .email(user.getEmail())
                        .name(user.getName())
                        .build();
            }
            return ResponseEntity.ok().body(new AuthenticationResponse(jwt));
    }

При запуске приложения у меня загружаются источники данных (dataSourceConfig.loadAllDataSources()). Это компонент, который возвращает карту tenant_id и соответствующие источники данных. Но когда я вызываю этот метод, источник данных по умолчанию, загружаемый в приложение через app.properties, равен где объект сотрудника — это место, где ищется метод findbyEmail(). Я ожидаю, что он будет искать его внутри арендатора, связанного с этим пользователем, но это не так.

Это приводит к ошибке, поскольку в моей базе данных по умолчанию нет таблицы сотрудников.

Я отредактировал TenantRouterClass. Теперь у меня есть класс ниже. Идея состоит в том, что при запуске приложения оно проверяет хранилище арендаторов по умолчанию, которое содержит информацию, используемую для создания других доступных источников данных.

package dev.iyanu.multitenancy.security_config;

import com.zaxxer.hikari.HikariDataSource;
import dev.iyanu.multitenancy.entities.Tenants;
import dev.iyanu.multitenancy.repository.TenantRepository;
import dev.iyanu.multitenancy.users.User;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Configuration
@Slf4j
public class TenantManager {

    private final TenantRepository tenantRepository;
    private final Map<Object,Object> tenantDataSources = new ConcurrentHashMap<>();
    private final DataSource dataSource;

    @Getter
    private Object currentTenant;
    private AbstractRoutingDataSource dataRouter;

    public TenantManager(TenantRepository tenantRepository, DataSource dataSource) {
        this.tenantRepository = tenantRepository;
        this.dataSource = dataSource;
    }

    //before you do a save or interact with the db call this method
    public void setCurrentTenant() {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.getPrincipal() instanceof User user){
            this.currentTenant = user.getTenantId();
        }
    }


    @PostConstruct
    public DataSource loadAllDataSources(){
        dataRouter = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return currentTenant;
            }
        };
        List<Tenants> tenantsList = tenantRepository.findAll();

        if (!tenantsList.isEmpty()){
            for(Tenants tenant : tenantsList){
                DataSource tenantDatasource = createDataSource(tenant);
                tenantDataSources.put(tenant.getTenantId(),tenantDatasource);
            }
        }
        tenantDataSources.forEach((tenantId, ds)->{
            var schemaInitializer = new ResourceDatabasePopulator(new ClassPathResource("schema.sql"));
            schemaInitializer.execute((DataSource) ds);
        });
        dataRouter.setDefaultTargetDataSource(dataSource);
        System.out.println("Default DataSource: "+dataSource);
        dataRouter.setTargetDataSources(tenantDataSources);
        System.out.println("DataSources loaded from my repo: "+tenantDataSources);
        dataRouter.afterPropertiesSet();
        return dataRouter;
    }


    public void addTenant(Tenants tenants){
        //create the Db for the Client
        createDatabase(tenants);

        //create datasource for the client
        DataSource newDataSource = createDataSource(tenants);

        //just to check that the new tenant added already has his datasource active
        try(Connection c = newDataSource.getConnection()){
            tenantDataSources.put(tenants.getTenantId(),newDataSource);
            dataRouter.afterPropertiesSet();
            System.out.println("The new tenant datasource is live");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }


    private DataSource createDataSource(Tenants tenants){
        var properties = new DataSourceProperties();
        properties.setPassword("root");
        properties.setUsername("root");
        properties.setDriverClassName("com.mysql.cj.jdbc.Driver");
        properties.setUrl("jdbc:mysql://localhost:3306/"+tenants.getDatabaseName());
        return properties.initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    private void createDatabase(Tenants tenants){
        try(Connection connection = dataSource.getConnection()){
            var databaseName = StringUtils.substringBefore(tenants.getName()," ");
            Statement statement = connection.createStatement();
            statement.executeUpdate("CREATE DATABASE "+databaseName);
            log.info("Database for {} created successfully", tenants.getName());
        }catch (SQLException e){
            e.printStackTrace();
            throw new RuntimeException();
        }
    }





}

Поэтому, когда пользователь пытается войти в систему, я аутентифицирую его, устанавливаю держателя контекста безопасности и вызываю метод setCurrentTenant() перед вызовом метода Emloyee.findByEmail(email). Я ожидаю, что метод вызовет источник данных, связанный с вызовом, но этого не произошло, он все равно направляется в установленную базу данных по умолчанию.


public ResponseEntity<AuthenticationResponse> loginUser(AuthenticateRequest authenticateRequest) {
            //authenticate the User
            authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(authenticateRequest.getEmail(),authenticateRequest.getPassword()));

            //this line of code runs oly if authentication is successful
            User user = userRepository.findByEmail(authenticateRequest.getEmail()).orElseThrow();
            String jwt = jwtService.generateToken(user);

            //set the security context to allow routing of db and to know the tenantID and also set the datasource
            SecurityContextHolder.getContext()
                    .setAuthentication(new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()));

            tenantManager.setCurrentTenant();
        Object currentTenant = tenantManager.getCurrentTenant();
        System.out.println(currentTenant);

        //create an Employee Object for the user once
            Employee employee = employeeRepository.findByEmail(user.getEmail());
            if (employee!= null){
                Employee newEmployee = Employee.builder()
                        .email(user.getEmail())
                        .name(user.getName())
                        .build();
            }
            return ResponseEntity.ok().body(new AuthenticationResponse(jwt));

    }

Печатаемый currentTenant — это тот, который связан с ожидаемым источником данных, но он по-прежнему не направляется в нужную базу данных и не направляется к целевому источнику данных по умолчанию.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
0
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Посмотрите на этот пример.

Я считаю, что вы упускаете здесь шаг, где вы указываете своему приложению, какой арендатор ему необходимо использовать для вашего сотрудника. Вы предоставляете своему мультитенантному источнику данных некоторые источники данных с помощью dataSourceConfig.loadAllDataSources(), но вы не указываете ему, какой из них использовать, поэтому он использует источник по умолчанию.

Я думал, что ключ определения поиска должен появиться и проверить ключ поиска, который является идентификатором арендатора, а затем направить его к источнику данных, который сопоставляется с этим ключом поиска?

Omotoso Iyanu 06.05.2024 10:32

@OmotosoIyanu Действительно, это должно сработать. Не могли бы вы поделиться своим классом MultiTenantDataSourceRouter и конфигурацией application.properties? Мне интересно, почему вы создаете новый экземпляр MultiTenantDataSourceRouter и устанавливаете источники данных в своем контроллере.

Bakou 06.05.2024 11:12

@OmotosoIyanu эта ссылка также может помочь. Обратите внимание, что в обеих ссылках, которыми я поделился, есть метод setCurrentTenant() или что-то подобное, который они используют во время выполнения. Это то, чего, я думаю, вам не хватает, если ваша конфигурация в MultiTenantDataSourceRouter верна.

Bakou 06.05.2024 11:44

Спасибо вам за ссылки. Я изучаю их, но не хочу использовать ThreadLocal непосредственно на своем этапе, я хочу использовать SecurityContextHolder, чтобы Spring мог управлять потоком, и я мог просто использовать эту информацию для получения результата. Я сообщу, смогу ли я заставить его работать

Omotoso Iyanu 06.05.2024 12:23

Я отредактировал код, но результат тот же, я лучше объяснил в посте.

Omotoso Iyanu 06.05.2024 14:45

Спасибо за добавленные подробности. Почему вы используете @PostConstruct? можете ли вы попробовать заменить это аннотациями @Bean и @Primary? Это установит ваш мультитенантный источник данных в качестве используемого по умолчанию bean-компонента. Я предполагаю, что вы используете репозиторий JPA, верно? вам также может потребоваться добавить аннотацию @EnableJpaRepositories(basePackages = "com.your.repository") в ваш класс конфигурации.

Bakou 06.05.2024 15:25

Я использую @PostConstruct, потому что в моем app.properties настроен источник данных, который будет загружаться как база данных по умолчанию. Если я изменю его на @Bean, это приведет к циклической зависимости, потому что для загрузки этого компонента мне нужно будет выполнить вызов tenantRepository, который также зависит от источника данных компонента. Поэтому я использовал @PostConstruct, поэтому, когда основной источник данных загружается, я могу загрузить другие источники данных. Это связано с тем, что информация об источнике данных для загрузки находится внутри репозитория арендатора TenantRepository

Omotoso Iyanu 06.05.2024 15:32

Есть ли причина, по которой вы сохраняете источник данных по умолчанию как отдельный компонент? \ dataRouter.setDefaultTargetDataSource(dataSource) не нужен конкретный компонент для вашего источника данных по умолчанию. Вы можете просто иметь частный метод в своем классе TenantManager, который создает ваш источник данных. Затем просто вызовите его при настройке источника данных по умолчанию dataRouter.setDefaultTargetDataSource(getMyDefaultDatasource‌​())

Bakou 06.05.2024 15:52

Поскольку в моем проекте есть другие классы, в которые внедрены репозитории, и они также являются bean-компонентами либо @Config, либо @Service, у меня возникают проблемы с циклическими зависимостями. Это происходит из-за того, что источник данных по умолчанию еще не загружен внутри компонента.

Omotoso Iyanu 06.05.2024 16:40

Используют ли другие репозитории разные источники данных? Или они используют стандартный и/или мультитенантный?

Bakou 07.05.2024 08:57

Итак, у меня есть один репозиторий, содержащий информацию о мультитенантных источниках данных. Итак, я наконец-то смог решить эту проблему. Ссылка, которую вы разместили, очень помогла. По сути, мне пришлось сначала настроить два разных источника данных при запуске приложения и предоставить конфигурации транзакций и объектных компонентов. Большое спасибо.

Omotoso Iyanu 07.05.2024 18:27

Другие вопросы по теме