Внедрение многопользовательской архитектуры на основе Mybatis-plus | Java вставка

задняя часть

Эта статья участвует в «Месяце тем Java — составление вопросов по Java», подробнее см.Ссылка на мероприятие

Мульти аренды (Multi-Tenant) является важной концепцией в SaaS. Это технология архитектуры программного обеспечения. В среде нескольких арендаторов один и тот же экземпляр системы используется совместно, а данные между арендаторами изолированы, то есть один арендатор не может получить доступ к данным другого арендатора. На основе различных уровней изоляции обычно существуют следующие три схемы реализации:

1. Каждый арендатор использует независимыйDataBase, высокий уровень изоляции, хорошая производительность, но высокая стоимость

2. Разделение между арендаторамиDataBase, используя отдельныйSchema

3. Разделение между арендаторамиSchema, добавив в таблицу поле арендатора с наивысшей степенью общих данных и самым низким уровнем изоляции.

Mybatis-plusНа уровне изоляции уровня 3 предоставляется мультиарендное решение на основе подключаемых модулей подкачки, которое мы представим. Перед официальным запуском сначала проведите подготовительную работу, создайте две таблицы и добавьте поле арендатора после базового поля.tenant_id:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  `address` varchar(64) DEFAULT NULL,
  `tenant_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
)
CREATE TABLE `dept` (
  `id` bigint(20) NOT NULL,
  `dept_name` varchar(64) DEFAULT NULL,
  `comment` varchar(128) DEFAULT NULL,
  `tenant_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
)

Импортируйте необходимые зависимости в проект:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>3.1</version>
</dependency>

Класс конфигурации Mybatis-plus:

@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

        List<ISqlParser> sqlParserList=new ArrayList<>();
        TenantSqlParser tenantSqlParser=new TenantSqlParser();
        tenantSqlParser.setTenantHandler(new TenantHandler() {
            @Override
            public Expression getTenantId(boolean select) {               
                String tenantId = "3";
                return new StringValue(tenantId);
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            @Override
            public boolean doTableFilter(String tableName) {
                return false;
            }
        });

        sqlParserList.add(tenantSqlParser);
        paginationInterceptor.setSqlParserList(sqlParserList);
        return paginationInterceptor;
    }
}

Основные реализованные здесь функции:

  • Создайте коллекцию парсеров SQL

  • Создать анализатор SQL арендатора

  • Настройте обработчик арендатора так, чтобы он специально обрабатывал логику арендатора.

На данный момент идентификатор арендатора зафиксирован как 3 для тестирования. Тест выполняет полный оператор таблицы:

public List<User> getUserList() {
    return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
}

Используйте подключаемый модуль для анализа выполняемого оператора SQL, и вы увидите, что условие фильтра арендатора автоматически добавляется после условия запроса:

Итак, в реальном проекте, как передать информацию об арендаторе обработчику арендатора, мы можем получить ее из кеша или заголовка запроса в зависимости от ситуации, взяв заголовок запроса в качестве примера:

@Override
public Expression getTenantId(boolean select) {
    ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    String tenantId = request.getHeader("tenantId");
    return new StringValue(tenantId);
}

Когда внешний интерфейс инициирует HTTP-запрос, в заголовок добавляется поле tenantId. После того, как серверная часть получает его в процессоре, оно устанавливается в качестве условия фильтрации клиентов текущего запроса.

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

@Override
public List<User> getUserListByFuture() {
    Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
    FutureTask<List<User>> future=new FutureTask<>(getUser);
    new Thread(future).start();
    try {
        return future.get();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

Выполняя вышеуказанный метод, можно увидеть, что текущий запрос запроса не может быть получен, поэтому невозможно получить идентификатор арендатора, что приведет к последующему исключению нулевого указателя ошибки:

Модификация также очень проста, откройте общий доступ к подпотоку RequestAttributes и измените приведенный выше код:

@Override
public List<User> getUserListByFuture() {
    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    Callable getUser=()-> {
        RequestContextHolder.setRequestAttributes(sra, true);
        return userMapper.selectList(new LambdaQueryWrapper<User>().isNotNull(User::getId));
    };
    FutureTask<List<User>> future=new FutureTask<>(getUser);
    new Thread(future).start();
    try {
        return future.get();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

После этой модификации информацию об арендаторе можно будет нормально получать в асинхронных потоках.

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

1. Если все операции SQL над всей таблицей не нужно выполнять над арендаторами, то отфильтруйте таблицу, измените метод doTableFilter и добавьте имя таблицы:

@Override
public boolean doTableFilter(String tableName) {
    List<String> IGNORE_TENANT_TABLES= Arrays.asList("dept");
    return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName));
}

Таким образом, все запросы в таблице dept не фильтруются:

2. Если есть какие-то конкретные операторы SQL, которые не хотят фильтроваться исполняющим тенантом, их можно открыть в виде аннотации @SqlParser, при этом аннотацию можно добавить только к методам интерфейса Mapper:

@SqlParser(filter = true)
@Select("select * from user where name =#{name}")
User selectUserByName(@Param(value="name") String name);

Либо укажите метод, который нужно отфильтровать в пейджинговом перехватчике:

@Bean
public PaginationInterceptor paginationInterceptor() {
    PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    paginationInterceptor.setSqlParserFilter(metaObject->{
        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
        // 对应Mapper、dao中的方法
        if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){
            return true;
        }
        return false;
    });
    ...
}

Вышеупомянутые два метода выполняют одну и ту же функцию, но если нужно отфильтровать много SQL-операторов, второй метод будет сложнее настроить, поэтому рекомендуется фильтровать по аннотации.

Кроме того, есть еще одна яма, на которую легче наступить: при копировании bean-компонентов не копируйте поле tenant id, иначе оператор SQL сообщит об ошибке:

public void createSnapshot(Long userId){
    User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, userId));
    UserSnapshot userSnapshot=new UserSnapshot();
    BeanUtil.copyProperties(user,userSnapshot);
    userSnapshotMapper.insert(userSnapshot);
}

Глядя на отчет об ошибке, можно увидеть, что когда поле tenant самого bean-компонента не пусто, SQL автоматически добавляет условие запроса tenant, что приводит к ошибке:

Мы можем изменить оператор копирования bean-компонента и вручную игнорировать поле идентификатора арендатора.Здесь мы используем класс инструментов BeanUtil hutool, который может добавлять поля игнорирования.

BeanUtil.copyProperties(user,userSnapshot,"tenantId");

После игнорирования копии идентификатора арендатора запрос может выполняться в обычном режиме.

Наконец, давайте взглянем на поддержку запросов к таблице соединений, сначала взглянем на SQL, содержащий подзапросы:

@Select("select * from user where id in (select id from user_snapshot)")
List<User> selectSnapshot();

Глядя на результаты выполнения, можно увидеть, что внутри подзапроса также автоматически добавляются условия запроса арендатора:

Давайте взглянем на запрос таблицы соединений с помощью Join:

@Select("select u.* from user u left join user_snapshot us on u.id=us.id")
List<User> selectSnapshot();

Точно так же условия фильтрации арендаторов будут добавлены как в левую, так и в правую таблицы:

Давайте еще раз взглянем на обычный запрос таблицы соединения без соединения:

@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null")
List<User> selectSnapshot();

Глядя на результаты выполнения, можно увидеть, что в этом случае в первую таблицу после ключевого слова FROM добавляется только условие фильтра арендатора, поэтому при использовании этого метода запроса нужно обратить особое внимание, а пользователь необходимо вручную добавить его в оператор SQL Добавить фильтр арендатора.