news 2026/9/24 22:43:42

彻底搞懂MyBatis关联映射:association与collection实战与源码剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彻底搞懂MyBatis关联映射:association与collection实战与源码剖析

做后端这些年,MyBatis的关联映射翻车现场我见过太多:明明SQL join出来了,结果对象里子集合是空的;加了resultMap之后,分页总数突然对不上了;还有那种日志里疯狂刷同一条SQL的N+1问题。这中间最核心的两个标签就是associationcollection,一个管“有一个”,一个管“有多个”。这篇我就把这两个标签从基础配置、嵌套查询、性能问题到源码级别的组装逻辑彻底讲透,适合刚接触MyBatis关联查询的人入门,也适合写了好几年XML但一直靠复制粘贴、没系统捋过的人查漏补缺。

1. 关联映射不是“会写JOIN”就万事大吉:三个翻车现场

很多人的第一反应是:关联映射不就是SQL写个JOIN,然后MyBatis自动把结果装进对象里吗?但实际跑起来会发现完全不是这么回事。我先说三个我真实遇到过的坑,每个都对应后面会展开的某个核心机制。

1.1 翻车现场一:Article对象里死活装不进那个Author

我当时接手的项目里,文章列表要展示作者名,SQL铁定是JOIN了author表,结果集里也有author_name列。但Article实体里的Author属性始终是null。

原因在于MyBatis的自动映射只处理“扁平”字段,它把author_name映射到Author.authorName需要绕过一层对象引用,自动映射根本没有这个能力。这时就必须用association标签告诉MyBatis:“这个author列,请给我填到Article里面的Author对象里去”。

1.2 翻车现场二:所有订单的ID都变成了用户ID

这是association里最经典的错误:父表和子表都有id列。默认情况下,MyBatis遇到同名列,后面列的值会把前面列的值覆盖掉。当你查询SELECT u.*, o.* FROM user u LEFT JOIN order o ON ...时,如果Order对象里的id没有显式指定来源列,它拿到的极可能是user.id

我当时排查了很久,最后发现order表的数据全乱套了,不是SQL错了,是association内部的列映射没写清楚,导致MyBatis按同名规则“张冠李戴”。这也是为什么我后面会一再强调:关联映射的列必须显式映射,哪怕你觉得字段名一样

1.3 翻车现场三:一对多查询后,分页插件统计出的数量完全不对

一对多场景更隐蔽:比如一个订单有3个商品,你JOIN出来后结果集有3条物理记录,但业务上这是1个订单。PageHelper这类分页插件在对SQL做COUNT时,统计的是JOIN后的行数,于是你会看到“总记录数=3”,可实际上只有一个订单。

这个问题不是分页插件的bug,而是关联映射本身会放大结果集行数。后面第三章我会专门讲一对多场景下分页的正确处理思路,这里先记住一个结论:一对多JOIN天然会放大行数,分页前必须先想清楚你要分页的粒度是什么

2. association:一对一/多对一场景的细节拆解

association对应的是“has one”语义:一个Article属于一个Author,一个Order属于一个User。它有两种主要使用方式:嵌套结果映射和嵌套查询。

2.1 嵌套结果映射:字段怎么对应,列别名怎么起

嵌套结果映射的意思是:SQL里已经把关联数据查出来了,MyBatis负责把结果集里的列“分拣”到两个对象里。典型的写法是这样:

<resultMap id="ArticleWithAuthorMap" type="com.example.Article"> <id property="id" column="article_id"/> <result property="title" column="title"/> <association property="author" javaType="com.example.Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> </association> </resultMap> <select id="selectArticleWithAuthor" resultMap="ArticleWithAuthorMap"> SELECT a.id AS article_id, a.title AS title, au.id AS author_id, au.name AS author_name FROM article a LEFT JOIN author au ON a.author_id = au.id WHERE a.id = #{id} </select>

这里有几个关键点:

第一,SQL里一定要给关联字段起别名a.id AS article_idau.id AS author_id,这样结果集里每个列的名字都是唯一的,不会出现我前面说的“订单ID变成用户ID”的情况。

第二,association里的column必须和SQL别名一致。上面的column="author_id"对应的是AS author_id,也就是说,resultMap里列的对应关系是直接跟SQL输出列名挂钩的,而不是跟实体字段名挂钩。这个关系搞错,关联对象的值就是null。

第三,javaType其实可以不写,因为MyBatis可以通过反射从Article.author属性的类型推断出来。但写上有两个好处:一是让阅读XML的人一眼看清类型,二是在某些没有泛型信息或构建工具链的极端场景下避免推断失败。我的建议是写上,成本很低。

association还支持在内部继续嵌套resultassociation甚至collection,所以一个文章实体可以同时携带作者、分类、评论列表。层级越深,结果集列名规划越要提前设计,后面第三章我会单独展开。

2.2 嵌套查询:什么时候该用,什么时候千万别用

嵌套查询是另一种思路:主查询先把Article查出来,然后针对每条Article再去执行另一条SQL查Author。写法如下:

<resultMap id="ArticleWithAuthorSelectMap" type="com.example.Article"> <id property="id" column="id"/> <result property="title" column="title"/> <association property="author" column="author_id" select="com.example.mapper.AuthorMapper.selectById"/> </resultMap> <select id="selectArticleWithAuthorSelect" resultMap="ArticleWithAuthorSelectMap"> SELECT id, title, author_id FROM article </select>

column="author_id"表示把当前结果集中的author_id列的值取出来,作为参数传给select指向的那条SQL。

嵌套查询最大的好处是主查询和子查询解耦:子查询可以被多个地方复用,SQL本身也很好维护。但它的致命缺点是N+1问题:查出N篇文章,就会执行N次作者查询,日志里会看到同一条SQL刷屏。如果文章列表有100条,就要额外执行100次数据库查询。

所以我的经验是:嵌套查询只适合在确认数据量小、或必须按需加载(比如子对象很少被用到)的场景使用。查询列表页这种高频场景,要么用嵌套结果映射一把梭,要么干脆拆成两条SQL在Service层组装。

2.3 fetchType和全局懒加载配置:一个参数决定性能

associationselect方式可以配合fetchType="lazy"实现懒加载:

<association property="author" column="author_id" select="com.example.mapper.AuthorMapper.selectById" fetchType="lazy"/>

加了lazy之后,查询Article列表时并不会立刻去查Author,而是等代码里真正调用article.getAuthor()时才触发那条SQL。这里有个前提:全局配置里要允许懒加载。相关配置有两项:

mybatis.configuration.lazy-loading-enabled=true mybatis.configuration.aggressive-lazy-loading=false

lazy-loading-enabled不言而喻,aggressive-lazy-loading的作用很多新手会忽略。当它等于true时,哪怕只访问article.getTitle(),也会把整个对象的所有懒加载属性全部加载出来,等于没懒。要把它设为false,才能做到“访问哪个懒属性才加载哪个”。

不过这只是理论上的“按需”,实际项目中我会默认关掉懒加载,原因很简单:懒加载必须在SqlSession还开着的时候触发。如果你在Service层事务内访问没问题,但一旦出了事务,或者用了某些异步线程,再碰懒加载属性就会抛org.apache.ibatis.exceptions.PersistenceException,提示SqlSession is already closed。这种运行时错误比N+1更难排查。

3. collection:一对多场景的正确打开方式

collection对应的是“has many”语义:一个Author有多篇文章,一个Order有多个商品。用法和association高度相似,但有几个独特的坑。

3.1 ofType、javaType到底怎么填

先看一段标准配置:

<resultMap id="AuthorWithArticlesMap" type="com.example.Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> <collection property="articles" ofType="com.example.Article"> <id property="id" column="article_id"/> <result property="title" column="title"/> </collection> </resultMap>

ofType表示集合里装的是哪种元素类型,这里是Article。很多新手会问:那javaType呢?javaType是表示集合本身的类型,默认MyBatis会给你填一个ArrayList。只有当你的实体属性声明的是接口类型(如List<Article>)且你希望MyBatis帮你new一个特定实现类时才需要指定,比如javaType="java.util.LinkedList"

需要记住的口诀是:javaType说的是“容器”,ofType说的是“容器里的内容”。绝大多数场景只写ofType就够了。

3.2 嵌套结果实现一对多的列前缀策略

用嵌套结果实现一对多时,核心还是列名唯一性。看这个例子:

<resultMap id="OrderWithItemsMap" type="com.example.Order"> <id property="id" column="order_id"/> <result property="orderNo" column="order_no"/> <collection property="items" ofType="com.example.OrderItem"> <id property="id" column="item_id"/> <result property="productName" column="product_name"/> <result property="quantity" column="quantity"/> </collection> </resultMap> <select id="selectOrderWithItems" resultMap="OrderWithItemsMap"> SELECT o.id AS order_id, o.order_no AS order_no, oi.id AS item_id, oi.product_name AS product_name, oi.quantity AS quantity FROM orders o LEFT JOIN order_item oi ON o.id = oi.order_id WHERE o.id = #{id} </select>

这里我用了order_iditem_id这样的语义化前缀,而不是o_idoi_id,因为这不仅是给MyBatis看的,也是给半年后的自己看的。一旦你忘了加前缀,导致id列重复,后果立刻就来了——下一节讲。

3.3 三层嵌套:先设计好列别名,再动手写XML

真实业务中,层级往往不止两层。比如:查询文章详情,需要带出作者信息,还要带出这条文章的所有评论,每条评论要带出评论者的用户信息。这就是一个典型的“association里套collection,collection里再套association”结构。

我现在的习惯是先画“列名规划表”再写XML。比如这种结构:

  • 文章:a_id, a_title
  • 作者:au_id, au_name
  • 评论列表:c_id, c_content
  • 评论者:u_id, u_nickname

然后SQL大概长这样:

SELECT a.id AS a_id, a.title AS a_title, au.id AS au_id, au.name AS au_name, c.id AS c_id, c.content AS c_content, u.id AS u_id, u.nickname AS u_nickname FROM article a LEFT JOIN author au ON a.author_id = au.id LEFT JOIN comment c ON c.article_id = a.id LEFT JOIN user u ON c.user_id = u.id WHERE a.id = #{id}

XML里逐层嵌套。层级一多,最容易出的问题是:某个子对象的id列写错了前缀,导致结果集里的评论数不对、用户信息串了。所以我的建议很简单——嵌套层数超过两层时,优先考虑用嵌套查询,或者在Service层手动装配。别硬刚,硬刚的代价是排查成本指数级上升。

3.4 一对多场景碰上分页插件:经典翻车与标准解法

回到开头说的分页问题。一个订单有3个商品,JOIN后3行,PageHelper的count会算成3,limit也会作用在JOIN后的临时表上,最终导致每页订单数不符。

我总结过三种解法,按推荐程度排序:

方案做法适用场景
先分页主表,再查子表先用分页插件查orders主表ID列表,再用WHERE order_id IN (...)查所有子表数据,最后在内存中组装大多数业务场景,推荐
嵌套查询+懒加载collectionselect配合fetchType="lazy",只对当前页的订单加载商品数据量小、可接受N+1的场景
JOIN后内存去重直接JOIN查询,然后对结果集去重数据量小、只查单页的场景

第一种是性能和可控性最均衡的。步骤是:

  1. orders表本身执行分页查询,得到当前页订单。
  2. 收集订单ID列表。
  3. SELECT * FROM order_item WHERE order_id IN (...)查出所有相关商品。
  4. Service层把商品按订单ID分组,set进对应订单对象。

这套方案的SQL简单清晰,也能配合缓存,比任何高级映射技巧都稳。MyBatis的关联映射确实能省事,但它不是用来包治百病的

4. 性能红线:N+1查询、懒加载和二级缓存之间的博弈

关联映射做得多了,你就会发现真正难的从来不是怎么写resultMap,而是怎么控制SQL执行次数和数据一致性。

4.1 N+1怎么定位:不要凭感觉,先看SQL日志

N+1问题的症状非常典型:页面上一个列表接口,撑死查了10条主记录,结果数据库连接池被打满,日志里刷了一模一样的SQL几十遍。我在项目里帮同事定位过这类问题,最快的方法不是看代码逻辑,而是把MyBatis的SQL日志打出来,看执行序列:

==> Preparing: SELECT * FROM article WHERE ... ==> Preparing: SELECT * FROM author WHERE id = ? ==> Parameters: 1(Long) ==> Preparing: SELECT * FROM author WHERE id = ? ==> Parameters: 2(Long) ...

看到这种“一条主查询后面跟着N条相同子查询”的规律,基本可以断定是嵌套查询写法没控制好触发时机。这时候优先确认:是否用了fetchType="lazy"?代码里是否在循环里调用了getAuthor()?如果都在合理范围内,那就要考虑换成嵌套结果映射或拆两条SQL。

4.2 懒加载的代理机制与事务边界

MyBatis实现懒加载,并不是简单地“先不查”,而是给关联对象生成了一个代理对象。当你调用author.getName()这类getter时,代理对象会拦截调用,触发后续SQL。这个代理机制的底层是ResultLoader和动态代理/CGLIB。

但这套机制有一个硬约束:触发SQL时必须有一个活着的SqlSession。我在实际项目中踩过的坑是:Service层方法加了@Transactional,事务内一切正常;后来把查询逻辑抽到一个没有事务的私有方法里,或者用@Async异步线程去访问懒加载属性,直接报Session已关闭。

所以如果项目是Spring Boot + MyBatis,用懒加载之前一定要想清楚事务边界。另外,@Transactional只是其中一环,真正决定生命周期的是SqlSession的开启和关闭时机。MyBatis-Spring整合时,SqlSessionTemplate通常每个事务/每次请求会打开和关闭SqlSession,事务外访问懒加载属性风险很高。

4.3 二级缓存对关联对象“不友好”的第二个原因

很多人以为加了二级缓存就万事大吉,但二级缓存有个隐藏问题:它缓存的是查询出来的对象本身,如果这个对象内部含有懒加载代理,就可能出现两种情况:

  1. 代理对象脱离SqlSession后不可用,缓存命中后直接取出来却访问不了子属性。
  2. 对象需要被序列化(比如你用Redis做二级缓存),但MyBatis生成的代理对象不一定能正常序列化。

另外,二级缓存是基于namespace的。如果你的ArticleMapper查询带出了作者信息,底层SQL涉及了author表,但AuthorMapper的缓存没失效,当author表被更新时,ArticleMapper缓存里的旧数据依然是脏的。这就是“关联查询与缓存失效”的矛盾。我在生产环境里吃过这个亏,所以项目里大多时候只开一级缓存,二级缓存只在单表查询、并发读多写少的场景下启用,关联查询尽量不用。

4.4 我的优化顺序:先减查询次数,再谈缓存

做关联查询性能优化,我的顺序一直很固定:

  1. 打开SQL日志,数清楚“一次业务请求实际执行了多少条SQL”。
  2. 如果执行条数是“1 + N”,优先合并:能用JOIN就JOIN,不能JOIN就用IN查询批量组装。
  3. 如果合并后单条SQL数据量过大(结果集膨胀严重),改用“主表查询 + IN子查询 + 内存组装”。
  4. 最后才是考虑加缓存,而且优先加业务层缓存(比如Redis),不是无脑开MyBatis二级缓存。

这个顺序帮我避开了很多为了性能优化反而引入新bug的情况。

5. 源码级复盘:MyBatis到底怎么把JOIN结果拼成对象图

想彻底搞懂associationcollection的边界行为,光会用不够,我建议花点时间看一眼MyBatis的结果集处理源码,不少“玄学”问题都能解释清楚。

5.1 从MapperProxy到DefaultResultSetHandler的调用链

一条Mapper接口方法调用,内部经过的路径大致是:

MapperProxy.invoke() -> MapperMethod.execute() -> SqlSession.selectOne/selectList() -> DefaultSqlSession.selectList() -> Executor.query() -> StatementHandler.query() -> ResultSetHandler.handleResultSets() -> DefaultResultSetHandler

最终,结果集组装的重活全在DefaultResultSetHandler里。它是MyBatis里另一个容易被人忽略的核心类,如果你经常排查结果映射问题,一定要会看它的代码。

5.2 getRowValue与applyNestedResultMappings:重活都在这

DefaultResultSetHandler处理一行数据时,主要做几件事:

handleRowValues() -> handleRowValuesForSimpleResultMap() // 普通映射(无嵌套) -> handleRowValuesForNestedResultMap() // 嵌套结果映射(association/collection)

handleRowValuesForNestedResultMap里,有一个非常关键的设计:它会根据<id>列的值构造一个CacheKey,用来判断“当前这一行是否属于前面已经构建过的那个父对象/子对象”。如果CacheKey命中,MyBatis会把新行数据并入已有对象;如果不命中,才会创建新对象。

我贴一段这段逻辑简化后的示意(不是完整源码,但足够理解思路):

// 伪代码:判断当前行是否已经构建过 if (cacheKey != null && alreadyThere == null) { // 说明没构建过,创建新对象并放入结果 } else { // 说明是同一对象的不同关联记录,复用已有对象,继续填充集合 }

这段机制解释了几乎所有嵌套结果映射的行为特征:<id>标记不是摆设,它是MyBatis用来“去重和聚合”的钥匙。

5.3 没写id标记,为什么一对多结果莫名“多行”

回到一个实际问题:缺失<id>标记会导致什么?源码逻辑看下来,答案很直接——MyBatis判断不了“这两行是不是同一个订单”,于是每一行都会被当成新订单来创建对象。最终,一个3行商品的订单,会被组装成3个一模一样的订单对象。

所以代码审查时,我第一眼看嵌套结果映射的resultMap,一定是先找每个层级有没有<id>标记。没有的话,不管当时跑起来对不对,早晚会出问题。这里说的<id>标记,是resultMap里的<id property="id" column="xxx"/>,不是数据库里的主键注释。

5.4 顺带聊聊ResultLoader:懒加载代理对象的诞生

associationcollection配置了嵌套查询且fetchType="lazy"时,MyBatis会创建一个ResultLoader对象,再把它包装成代理对象。一切的关键都在这个ResultLoader身上:它会在被代理的方法被调用时,拿到之前保存的SqlSession、MappedStatement、参数值去执行真正的查询。

这也是为什么“SqlSession必须活着”会成为硬性要求——ResultLoader持有的是SqlSession或能从SqlSession中获取信息的能力,Session一关,加载动作便无从执行。了解了这层原理,你就知道前面说的“懒加载必须在事务内使用”并不是危言耸听。

6. 多年下来沉淀的关联映射自查清单

最后把这些年踩过的坑沉淀成一份清单。每次写完关联映射,或者Review同事代码,我都会走一遍这套检查。

6.1 动手前先回答这5个问题

  1. 业务语义是“属于”还是“包含”?属于用association,包含用collection
  2. 关联字段和父表字段会重名吗?会重名就一定要在SQL里起别名,并且在resultMap里显式映射。
  3. 一次查询会产生多少行物理记录?如果超过1行,需要确认这是不是业务想要的粒度,尤其涉及分页时必须先想清楚。
  4. 数据量级多大?小表随便嵌套查询,大表优先考虑合并SQL或分批查询。
  5. 关联对象是否会被事务外访问?是的话不要用懒加载。

6.2 容易被忽略的配置组合

有几个配置单独看都很基础,但组合起来经常让人栽跟头。

首先是mapUnderscoreToCamelCase。设为true时,列名author_name能自动映射到authorName。但它只对普通result属性有效,对嵌套对象里的列名依然要求“列名和resultMap里的column一致”或“列名能自动映射到子对象属性”。所以听到“我开了驼峰映射为什么关联对象还是null”这种问题,先查association里的column是否跟SQL列名一致。

其次是autoMappingBehavior。默认是PARTIAL,会自动映射非嵌套对象的属性。但一旦你使用了resultMap,并且这个resultMap没有写全所有属性,自动映射行为可能会让你的字段“时有时无”。我的做法是:核心字段全显式映射,附属字段不写死,交给自动映射,但要清楚自动映射的边界。

最后是Oracle数据库。如果你用Oracle,未起别名的列在结果集里是大写的(比如ID),而MyBatis的column映射是区分大小写的?其实MyBatis内部有大小写转换逻辑,但在嵌套映射场景下我依然遇到过大小写不匹配的问题,建议还是显式起别名、显式写column,不要依赖隐式行为。

6.3 调试时我是怎么快速定位的

碰到关联映射结果不对,我的调试步骤是:

  1. 先打印完整SQL日志,确认SQL本身查出来的数据对不对。可以用数据库客户端直接执行这条SQL,数一下行数和值。
  2. 看resultMap的id标记是否齐全,尤其是collection场景。
  3. 把resultMap简化到最小可复现,先只保留一个层级,跑通了再加一层。多层嵌套怎么都查不出问题时,用这套“减法”往往一下就定位到了。
  4. 检查别名是否重复,尤其是手写了AS xxx但后面还有同名字段的情况。

最后说个不算技巧的技巧:MyBatis的XML写多了,有时候报错信息很笼统,像“Result Maps collection does not contain value for ...”,这种一般是resultMap的id写错了,或者是<association>里的select属性指向的Mapper方法不存在。先把这些拼写层面的事情确认完,再往业务逻辑上找,效率会高很多。

关联映射本身不难,真正考验人的是对数据粒度、SQL执行次数、对象生命周期这些横切面的把控。我写完这篇之后,自己再看老项目里的各种resultMap,发现大多数性能隐患和诡异数据问题,都能从“有没有滥用嵌套查询”、“id标记是否齐全”、“是不是在JOIN结果上做分页”这三件事里找到答案。希望你也能带着这几个问题去review自己的代码,少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 22:43:26

YOLO安全帽手套检测数据集:三种格式标签与完整训练指南

简介&#xff1a;面向目标检测初学者与工业安全场景开发者&#xff0c;YOLO安全帽手套检测数据集提供真实场景下5000张高质量图片&#xff0c;覆盖工地、厂区等多种作业环境&#xff0c;并配套VOC、COCO、YOLO三种格式标签&#xff0c;标注框质量高&#xff0c;可直接用于YOLO系…

作者头像 李华
网站建设 2026/9/24 22:43:11

MACE端侧深度学习推理框架架构解析与部署实战

1. 端侧部署为什么这么难——MACE的设计初衷1.1 端侧场景和云端场景完全是两回事干了这些年移动端AI&#xff0c;我最大的感受就是&#xff1a;很多人把端侧部署想得太简单了。在云端&#xff0c;你有一堆GPU、有充足的内存、有无限的电量&#xff0c;最多就是多花点钱的事。但…

作者头像 李华
网站建设 2026/9/24 22:42:54

基于Python和PyQt5的超市商品管理系统设计与实现

超市商品管理系统&#xff0c;听起来是个被做烂了的课设题目&#xff0c;但真正动手写过的人都知道&#xff0c;从“能跑”到“能用”之间隔着的距离&#xff0c;比你想象的要远得多。市面上绝大多数教程和现成代码&#xff0c;要么是纯控制台黑窗口&#xff0c;数据全靠手敲&a…

作者头像 李华
网站建设 2026/9/24 22:42:49

Rokid Glasses AIUI开发实战:从零搭建“今天吃什么”语音助手

1. 从一副眼镜说起&#xff1a;为什么要在 Rokid Glasses 上折腾 AIUI第一次拿到 Rokid Glasses 的时候&#xff0c;我脑子里冒出来的第一个念头其实特别朴素——这玩意儿能不能帮我决定中午吃什么。别笑&#xff0c;这大概是每个打工人每天都要面对的灵魂拷问。而“今天吃什么…

作者头像 李华
网站建设 2026/9/24 22:41:31

SOA协议族核心解析:从WSDL、SOAP到WS-*与REST的选型实战

1. 认清SOA协议族的结构&#xff1a;先理解“为什么要协议&#xff0c;而不是只有接口”学15.4这一节&#xff0c;最怕的就是一头扎进WSDL、SOAP、UDDI这些缩写里出不来。我先说个结论&#xff1a;把这些协议当成“一堆要背的名词”去学&#xff0c;考完就忘&#xff0c;论文也…

作者头像 李华
网站建设 2026/9/24 22:41:03

2026 AI影视实战:一个人如何用AI智能体工作流做AI漫剧

1. 从"一个人一支队伍"说起&#xff1a;AI影视生产的底层逻辑变了2026年开年到现在&#xff0c;我身边至少有七八个朋友从传统影视后期、广告片拍摄、甚至游戏美术的岗位上"单飞"了。他们没租办公室&#xff0c;没签艺人&#xff0c;团队名单上就自己一个名…

作者头像 李华