R2dbc n+1问题的解决

前情提要:由于快要大三了,我准备做一个项目来作为毕业设计以及用来写到简历上。预计是用kotlin+java webflux R2dbc以及springcloud 的一个微服务项目。
我并不想使用mybatis,我更喜欢jpa,但是我认为Webflux和R2dbc的组合能够大大增加系统的吞吐量,因此我开始尝试R2dbc。在此过程中遇到了许多问题。

由于R2dbc不是一个orm,所以如何处理对象关系映射成为了一个很大的麻烦。不能像以前jpa一样自动完成,需要手动编写额外的代码进行转换。
假设有一个场景 我们有很多个老师,每个老师有若干个学生,每个学生只有一个老师。

假设我们需要查出所有老师的数据以及他们的学生的数据,我们编写以下实体类
在这里插入图片描述
学生实体类如下
在这里插入图片描述
使用了一个外键来维护多对一的关系。

教师表
在这里插入图片描述

学生表在这里插入图片描述

接下来 我们手写sql语句进行查询

在这里插入图片描述

这里使用了CoroutineRepository ,因此我们可以用kotlin以同步逻辑编写异步代码。
查询的结果显然无法被自动转换,我们需要手动编写一个Converter,在那之前 我们先来看看返回的数据长什么样子。
用datagrip,方便起见我们直接select*进行测试

select * from teacher left join student on teacher.id = student.teacher_id

在这里插入图片描述
这个是返回结果 其中红色框框是Teacher的字段 橙色是Student的字段,可以看到 一个老师对应多个学生。这意味着我们查询出来的结果中,有大量重复的Teacher数据。
我们可以根据相同的Teacher进行groupBy

类似这样进行分组
在这里插入图片描述
我们需要做的是把每一组中的Teacher合并成一个对象,然后属于Student的字段构建成一个List<Student>
让我们回头看看上面的类就能明确目标了
在这里插入图片描述

以下是Converter的代码
在这里插入图片描述
我们逐步从查询结果中取出字段,先填充个Teacher部分,然后如果有Student的字段(可能老师没有对应的学生),那么就新建一个Student对象放入TeacherWithStudents中

现在我们得到一个List对象,它的size等于查询的结果行数目。这个List有一个问题 它的每个TeacherWithStudents中 Teacher存在重复字段,并且每个之中只有一个Student(或者没有)。

来到了重点部分:把多个TeacherWithStudents进行合并。为了解决这个问题,我编写了一个注解和相应的处理器。
在这里插入图片描述

这个注解被用在List<Student>的属性上

在这里插入图片描述
如图所示 这代表标记了这个属性应该被合并。
我们来想一下应该怎么做。
我们现在有大量的TeacherWithStudents,他们之中只有students不同,并且students中只有至多一个Student。
我们先把Teacher属性部分相同的TeacherWithStudents挑出来放在一组,随便选一个作为最终结果,然后把该组中的其他成员的students中的Student加入进去即可。
这样我们就把一个List<TeacherWithStudents>合并成一个TeacherWithStudents了。

那么第一步,就应该是进行分组 我们根据Teacher的id进行groupby

    public static <T> Collection<Collection<T>> groupBy(Collection<T> objs,String fieldName){
        if(objs.isEmpty()){
            return null;
        }
        Object[] objects = objs.toArray();
        Class<?> aClass = objects[0].getClass();
        Map<Object,Collection<T>> group = new HashMap<>();
        try {

            Field keyField = aClass.getDeclaredField (fieldName);
            keyField.setAccessible(true);
            for (T obj : objs) {
                Object key = keyField.get(obj);
                if(group.containsKey(key)){
                    group.get(key).add(obj);
                }else{
                    List<T> list = new ArrayList<>();
                    list.add(obj);
                    group.put(key,list);
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return group.values();
    }

我们把一个Collection分组后变成Collection<Collection>,里面那个Collection代表一组Teacher部分相同的TeacherWithStudents对象 然后有若干组 装在外层的Collection

之后 我们要对每一组进行组内的合并,以下是代码

    public  static <T> T merge(Collection<T> objs){
        if(objs.isEmpty()){
            return null;
        }
        Object[] objects = objs.toArray();
        T res = (T)objects[0];
        Class<?> aClass = res.getClass();
        List<Field> fields = new ArrayList<>();
        for (Field field : aClass.getDeclaredFields()) {
            if (field.isAnnotationPresent(Mergeable.class)){
                field.setAccessible(true);
                fields.add(field);
            }
        }
        for (int i = 1; i < objects.length; i++) {
            for (Field field : fields) {
                try {
                    Collection value = (Collection)field.get(objects[i]);
                    Collection target = (Collection)field.get(res);
                    target.addAll(value);

                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        return res;
    }

我们在这个方法中扫描有刚刚定义的,Mergeable注解属性,把它们进行合并。这里我们选择了这一组中的0号元素作为返回的结果objects[0],通过反射把其他元素的相应属性加入到0号元素中,以完成合并。为了方便起见,我们额外定义一个方法,把初始的List直接转换成结果,并且返回自身,就像原地转换那样。

   public static <T> Collection<T> groupAndMerge(Collection<T> objs, String fieldName){
        Collection<Collection<T>> collections = groupBy(objs, fieldName);
        objs.clear();
        if(collections == null) return objs;
        collections.forEach(collection->objs.add(merge(collection)));
        return objs;
    }

我这边用kotlin的拓展函数把这个方法拓展到集合,方便调用,实际工作中不推荐这么做。

fun <T> Collection<T>.MergeCollections(fieldName:String):Collection<T>{
   return GroupAssembler.groupAndMerge(this,fieldName)
}

然后我们来测试一下结果,通过suspend函数使得不会阻塞住线程,而且这个异步代码看起来跟同步代码的逻辑是一样的

    @GetMapping("test10")
    suspend fun getTeachers10(): List<TeacherWithStudents>{
        return kotlinTeacherRep.myFindAll().MergeCollections( "id").toList();
    }

这里只截取一小部分

 {
    "id": 1,
    "name": "小李1",
    "students": [
      {
        "id": 1,
        "name": "学生1",
        "teacherId": 1,
        "teacher": null,
        "newProduct": false,
        "new": false
      },
      {
        "id": 201,
        "name": "学生1",
        "teacherId": 1,
        "teacher": null,
        "newProduct": false,
        "new": false
      }
    ],
    "newProduct": false,
    "new": false
  },
  {
    "id": 2,
    "name": "小李2",
    "students": [
      {
        "id": 2,
        "name": "学生2",
        "teacherId": 2,
        "teacher": null,
        "newProduct": false,
        "new": false
      },
      {
        "id": 202,
        "name": "学生2",
        "teacherId": 2,
        "teacher": null,
        "newProduct": false,
        "new": false
      }
    ],
    "newProduct": false,
    "new": false
  },

可以看到我们成功处理了1:N的关系映射。并且由于通过了注解+反射处理,使得可以很轻易地用在别的对象上。
以下是完整的实体类代码

@Data
@Table
public class Student implements Persistable<Long> {
    @Id
    Long id;
    String name;
    public Long teacherId;

    @Transient
    TeacherWithStudents teacher;


    @Transient
    private boolean newProduct;

    @Override
    @Transient
    public boolean isNew() {
        return this.newProduct || id == null;
    }

    public Student setAsNew() {
        this.newProduct = true;
        return this;
    }
}
@Data
@Table("teacher")
public class TeacherWithStudents implements Persistable<Long> {
    @Id
    Long id;
    String name;
    @Transient
    @Mergeable
    List<Student> students = new ArrayList<>();

    public TeacherWithStudents withStudents(List<Student> students) {
        this.students = students;
        return this;
    }


    public TeacherWithStudents addStudent(Student student) {
        if (students == null) {
            students = new ArrayList<>();
        }
        students.add(student);
        return this;
    }

    @Transient
    private boolean newProduct;

    @Override
    @Transient
    public boolean isNew() {
        return this.newProduct || id == null;
    }

    public TeacherWithStudents setAsNew() {
        this.newProduct = true;
        return this;
    }
}

<think>好的,用户现在想了解Project Reactor。首先,我需要回忆一下之前和用户讨论的内容。之前用户问过Spring WebFlux,而Project Reactor是WebFlux的核心依赖库。所以用户可能是在学习响应式编程,特别是Spring的相关技术,想深入理解底层原理。 用户可能是开发者,正在构建高并发的应用,或者在学习如何优化应用性能。他们可能已经知道Spring MVC,现在想转向响应式编程,但遇到了Project Reactor这个新概念,需要进一步了解。 接下来,我需要确定用户的真实需求。他们可能不仅仅想要一个简单的定义,而是想了解Reactor的核心概念、如何使用、优缺点以及实际应用场景。此外,用户可能还想知道Reactor与其他库(比如RxJava)的区别,或者如何在项目中应用它。 我需要组织信息,结构要清晰。先介绍Reactor是什么,然后核心概念比如Flux和Mono,背压机制,调度器。接着对比RxJava,适用场景,优缺点,最后给出示例代码。这样用户能逐步深入理解。 要注意之前用户提到过WebFlux,所以需要强调Reactor在其中的作用,比如作为响应式流的具体实现。同时,要避免技术术语过,用易懂的例子解释,比如用Flux.just()创建数据流。 还要提醒用户可能遇到的挑战,比如调试困难和学习曲线陡峭,这样他们能权衡是否适合项目使用。最后,提供示例代码帮助用户快速上手,并给出使用建议,比如在IO密集型场景使用。 需要检查是否有遗漏点,比如背压处理的具体机制,或者如何与Spring集成。确保回答准确,并且符合用户的学习路径,从概念到实践,逐步展开。</think>以下是关于 **Project Reactor** 的详细介绍: --- ### 一、核心定义 **Project Reactor** 是一个基于 **Reactive Streams 规范** 的响应式编程库,专为 Java 8+ 设计。它是 Spring WebFlux 的默认响应式引擎,用于构建非阻塞、高并发的异步应用。 --- ### 二、核心概念 1. **响应式流模型** - 提供两种核心容器: - **`Flux<T>`**:示包含 `0~N` 个元素的异步数据流(例如:数据库查询结果流)。 - **`Mono<T>`**:示包含 `0~1` 个元素的异步结果(例如:单个 HTTP 请求响应)。 ```java Flux<Integer> numbers = Flux.just(1, 2, 3); // 创建包含三个元素的流 Mono<String> name = Mono.just("Alice"); // 创建单个元素的流 ``` 2. **背压(Backpressure)** - 消费者通过主动请求(Pull)控制数据流速,避免生产者过载。 - 示例:当消费者处理速度较慢时,通过 `request(n)` 限制数据拉取量。 3. **操作符(Operators)** - 提供丰富的链式操作方法(类似 Java Stream API),如 `map`, `filter`, `flatMap` 等。 ```java Flux.range(1, 5) .map(i -> i * 2) // 转换每个元素 .filter(i -> i > 5) // 过滤元素 .subscribe(System.out::println); // 输出 6, 8, 10 ``` 4. **调度器(Schedulers)** - 控制线程执行模型(如并行、弹性线程池): - `Schedulers.immediate()`:当前线程执行。 - `Schedulers.parallel()`:固定大小线程池(适合 CPU 密集型任务)。 - `Schedulers.elastic()`:无界线程池(适合 I/O 密集型任务)。 --- ### 三、与 RxJava 对比 | 特性 | Project Reactor | RxJava | |-----------------------|------------------------------|----------------------------| | **目标平台** | Java 8+ | 语言(JavaKotlin等) | | **集成 Spring** | 官方支持(Spring WebFlux) | 需手动适配 | | **背压实现** | 原生支持 Reactive Streams | 通过扩展库支持 | | **调试工具** | 提供 `Hooks.onOperatorDebug` | 依赖第三方工具 | --- ### 四、适用场景 1. **高吞吐服务** 如实时推送系统(股票行情、聊天消息)。 2. **微服务通信** 非阻塞 HTTP 客户端(如 WebClient)调用其他服务。 3. **流式数据处理** 处理 Kafka 消息流或数据库变更流(Change Data Capture)。 --- ### 五、核心优势 1. **资源高效** - 少量线程处理高并发请求(如 Netty 的 EventLoop 模型)。 2. **无缝集成 Spring** -Spring WebFlux、Spring Data Reactive 深度整合。 3. **丰富的错误处理** - 提供 `onErrorReturn`, `onErrorResume` 等操作符优雅处理异常。 ```java Flux.just(1, 2, 0) .map(i -> 10 / i) .onErrorResume(e -> Flux.just(-1)) // 发生异常时返回-1 .subscribe(System.out::println); // 输出 10, 5, -1 ``` --- ### 六、局限性 1. **调试困难** 异步调用链的堆栈信息不直观。 2. **学习曲线陡峭** 需理解响应式编程思维(如数据流、延迟执行)。 3. **阻塞操作破坏模型** 若误用阻塞代码(如 `Thread.sleep()`),会导致性能下降。 --- ### 七、代码示例 ```java // 1. 创建响应式 REST 端点 @RestController public class UserController { @GetMapping("/users") public Flux<User> getUsers() { return userRepository.findAll(); // 非阻塞查询 } } // 2. 组合个流 Mono<User> user = userRepository.findById("1"); Mono<Order> order = orderRepository.findByUserId("1"); Mono<UserOrder> result = user.zipWith(order, (u, o) -> new UserOrder(u, o)); // 3. 使用调度器切换线程 Flux.range(1, 10) .parallel() .runOn(Schedulers.parallel()) // 切换到并行线程池 .map(i -> i * i) .subscribe(); ``` --- ### 八、使用建议 1. **优先场景** - 需要处理 `10,000+` 并发连接的 I/O 密集型服务。 - 已使用响应式数据库(如 MongoDB Reactive、R2DBC)。 2. **避免场景** - 依赖传统阻塞式库(如 JDBC、同步文件读写)。 - 团队缺乏响应式编程经验。 --- 通过 Project Reactor,开发者可以高效利用系统资源,但需权衡其复杂性与实际需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值