前情提要:由于快要大三了,我准备做一个项目来作为毕业设计以及用来写到简历上。预计是用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;
}
}