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;
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用r2dbc+mysql+springboot创建表需要以下步骤: 1. 配置MySQL连接信息,包括数据库名称、用户名、密码和MySQL的URL等。 2. 创建一个Java Bean,用于表示表中的字段和数据类型。例如,如果要创建一个名为person的表,可以在Java中创建一个名为Person的类,该类包含与表中字段对应的属性。 3. 创建一个Repository接口,用于定义插入、查询、更新和删除数据等操作。在接口中通过SQL语句进行定义,例如使用@Query注解定义插入数据的SQL语句。 4. 在Spring Boot应用程序启动时,使用Spring的自动化配置功能自动创建表。可以通过在应用程序的配置文件中添加spring.jpa.hibernate.ddl-auto属性来控制表的创建方式,例如可以将该属性设置为create或update等选项。 下面是一个简单的示例代码,用于创建一个名为person的表: 1. 在application.properties中添加MySQL数据库连接配置信息: ``` spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb spring.r2dbc.username=testuser spring.r2dbc.password=testpass ``` 2. 创建一个Person类,在其中定义表中的字段和数据类型: ```java @Data public class Person { private Long id; private String firstName; private String lastName; } ``` 3. 创建一个PersonRepository接口,在其中定义对person表的操作: ```java public interface PersonRepository extends ReactiveCrudRepository<Person, Long> { @Query("INSERT INTO person (first_name, last_name) VALUES (:firstName, :lastName)") Mono<Void> insert(@Param("firstName") String firstName, @Param("lastName") String lastName); @Query("SELECT * FROM person WHERE id = :id") Mono<Person> findById(@Param("id") Long id); } ``` 4. 在启动类中添加@EnableR2dbcRepositories注解,开启R2DBC仓库的自动化配置: ```java @SpringBootApplication @EnableR2dbcRepositories public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 5. 启动应用程序,Spring Boot将自动创建名为person的表。如果表已经存在,则不会进行任何操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值