JOOQ用法和实例
Posted BLKNjy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JOOQ用法和实例相关的知识,希望对你有一定的参考价值。
这里写目录标题
JOOQ简介
jOOQ,是一个ORM框架,利用其生成的Java代码和流畅的API,可以快速构建有类型约束的安全的SQL语句 。
优点:
jOOQ的核心优势是可以将数据库表结构映射为Java类,包含表的基本描述和所有表字段。通过jOOQ提供的API,配合生成的Java代码,可以很方便的进行数据库操作
生成的Java代码字段类型是根据数据库映射成的Java类型,在进行设置和查询操作时,因为是Java代码,都会有强类型校验,所以对于数据的输入,是天然安全的,极大的减少了SQL注入的风险
jOOQ的代码生成策略是根据配置全量生成,任何对于数据库的改动,如果会影响到业务代码,在编译期间就会被发现,可以及时进行修复
CRUD
所有的操作jooq都提供两种方式, 第一种是使用 DSLContext API 以类SQL的语法进行调用,第二种是利用 Record API 进行调用 。这里面只记录第一种,了解第二种请点击这里
了解:
dslContext
代表DSLContext
实例S1_USER
由jOOQ插件生成的表描述常量S1_USER.*
由jOOQ插件生成的表内字段常量了解三个接口:
org.jooq.Result
结果集接口,此接口实现了List接口,可以当做一个集合来操作,是一个数据库查询结果集的包装类,除了集合的相关方法,该接口还提供了一些结果集转换,格式化,提取字段等方法。通常我们查询出来的结果都是此接口的实现类,掌握好此接口是jOOQ的基础接口,基本所有的SQL查询操作,都会碰到这个接口org.jooq.Record
此接口再使用关系型数据库时,主要用于定义数据库表记录,储存的内容是一条表记录的字段和值,每个值会储存对应字段的类型,可以通过通用的getValue(Field field)
方法,取到对应字段的值,也可以将这个接口看做是一条记录的字段/值映射org.jooq.DSLContext
jOOQ的核心接口之一,可以理解为一个SQL执行器,通过静态方法DSL.using
,可以获取一个DSLContext
实例,此实例抽象了所有对于SQL的操作API,可以通过其提供的API方便的进行SQL操作
INSERT
// 类SQL语法 insertInto 方法第一个参数通常是表常量
dslContext.insertInto(S1_USER, S1_USER.USERNAME, S1_USER.ADDRESS, S1_USER.EMAIL)
.values("username1", "demo-address1", "diamondfsd@gmail.com")
.values("username2", "demo-address2", "diamondfsd@gmail.com")
.execute();
//批量插入
List<S1UserRecord> recordList = IntStream.range(0, 10).mapToObj(i ->
S1UserRecord s1UserRecord = new S1UserRecord();
s1UserRecord.setUsername("usernameBatchInsert" + i);
s1UserRecord.setEmail("diamondfsd@gmail.com");
return s1UserRecord;
).collect(Collectors.toList());
dslContext.batchInsert(recordList).execute();
//插入后获取主键
//通过此方法插入数据,可以通过 returning API读取想要返回的数据,此语法支持返回多个值,通过fetchOne()方法可以取到一个Record对象
Integer userId = dslContext.insertInto(S1_USER,
S1_USER.USERNAME, S1_USER.ADDRESS, S1_USER.EMAIL)
.values("username1", "demo-address1", "diamondfsd@gmail.com")
.returning(S1_USER.ID)
.fetchOne().getId();
//插入时主键重复的处理办法
// 第一种 :这里执行完,返回affecteRow影响行数为0,即不生效
// 生成的SQL: insert ignore into `learn-jooq`.`s1_user` (`id`, `username`) values (1, 'username-1')
int affecteRow = dslContext.insertInto(S1_USER,
S1_USER.ID, S1_USER.USERNAME)
.values(1, "username-1")
.onDuplicateKeyIgnore()
.execute();
// 第二种:更新主键所在列
//生成SQL: insert into `learn-jooq`.`s1_user` (`id`, `username`, `address`) values (1, 'duplicateKey-update', 'hello world') on duplicate key update `learn-jooq`.`s1_user`.`username` = 'duplicateKey-update', `learn-jooq`.`s1_user`.`address` = 'update'
dslContext.insertInto(S1_USER)
.set(S1_USER.ID, 1)
.set(S1_USER.USERNAME, "duplicateKey-insert")
.set(S1_USER.ADDRESS, "hello world")
.onDuplicateKeyUpdate()
.set(S1_USER.USERNAME, "duplicateKey-update")
.set(S1_USER.ADDRESS, "update")
.execute();
update
dslContext.update(S1_USER)
.set(S1_USER.USERNAME, "apiUsername-1")
.set(S1_USER.ADDRESS, "update-address")
.where(S1_USER.ID.eq(1))
.execute()
//批量更新
S1UserRecord record1 = new S1UserRecord();
record1.setId(1);
record1.setUsername("batchUsername-1");
S1UserRecord record2 = new S1UserRecord();
record2.setId(2);
record2.setUsername("batchUsername-2");
List<S1UserRecord> userRecordList = new ArrayList<>();
userRecordList.add(record1);
userRecordList.add(record2);
dslContext.batchUpdate(userRecordList).execute();
select
基本查询方法,默认查询指定表的所有字段,返回一个结果集的包装,通过
Result.into
方法,可以将结果集转换为任意指定类型集合,当然也可以通过Record.getValue
方法取得任意字段值,值类型依赖于字段类型
// select `learn-jooq`.`s1_user`.`id`, `learn-jooq`.`s1_user`.`username`, `learn-jooq`.`s1_user`.`email`, `learn-jooq`.`s1_user`.`address`, `learn-jooq`.`s1_user`.`create_time`, `learn-jooq`.`s1_user`.`update_time` from `learn-jooq`.`s1_user`
Result<Record> fetchResult = dslContext.select().from(S1_USER).fetch();
List<S1UserRecord> result = fetch.into(S1UserRecord.class);
// select `learn-jooq`.`s1_user`.`id`, `learn-jooq`.`s1_user`.`username`, `learn-jooq`.`s1_user`.`email`, `learn-jooq`.`s1_user`.`address`, `learn-jooq`.`s1_user`.`create_time`, `learn-jooq`.`s1_user`.`update_time` from `learn-jooq`.`s1_user` where `learn-jooq`.`s1_user`.`id` in (1, 2)
Result<Record> fetchAll = dslContext.select().from(S1_USER)
.where(S1_USER.ID.in(1, 2)).fetch();
fetchAll.forEach(record ->
Integer id = record.getValue(S1_USER.ID);
String username = record.getValue(S1_USER.USERNAME);
String address = record.getValue(S1_USER.ADDRESS);
Timestamp createTime = record.getValue(S1_USER.CREATE_TIME);
Timestamp updateTime = record.getValue(S1_USER.UPDATE_TIME);
);
jooq也支持关联查询
//UserMessagePojo为新建的Pojo类,用于存储查询结果,可以忽略具体内容
Result<Record3<String, String, String>> record3Result =
dslContext.select(S1_USER.USERNAME,
S2_USER_MESSAGE.MESSAGE_TITLE,
S2_USER_MESSAGE.MESSAGE_CONTENT)
.from(S2_USER_MESSAGE)
.leftJoin(S1_USER).on(S1_USER.ID.eq(S2_USER_MESSAGE.USER_ID))
.fetch();
List<UserMessagePojo> userMessagePojoList = record3Result.into(UserMessagePojo.class)
Condition动态查询
public void query(String name,String age)
Condition condition=DSL.trueCondition();//真实条件
if(name!=null)
condition=condition.and(Tables.STUDENT.NAME.eq(name));
if(age!=null)
condition=condition.and(Tables.STUDENT.AGE.eq(Integer.parseInt(age)));
List<Student> list=context.select().from(Tables.STUDENT).where(condition).fetch().into(Student.class);
Delete
dslContext.delete(S1_USER).where(S1_USER.USERNAME.eq("demo1")).execute();
//批量删除
S1UserRecord record1 = new S1UserRecord();
record1.setId(1);
S1UserRecord record2 = new S1UserRecord();
record2.setId(2);
dslContext.batchDelete(record1, record2).execute();
//
List<S1UserRecord> recordList = new ArrayList<>();
recordList.add(record1);
recordList.add(record2);
dslContext.batchDelete(recordList).execute();
结果处理
查询操作通常以fetch API 作为结束API,例如常用的有,所有的读取类方法都差不多,掌握一个就能很快的举一反三
- 读取多条
fetch
读取集合fetchSet
读取并返回一个Set集合,常用于去重fetchArray
读取并返回一个数组- 读取单条
fetchOne
读取单条记录,如果记录超过一条会报错fetchAny
读取单条记录,如果有多条,会取第一条数据fetchSingle
读取单条记录,如果记录为空或者记录超过一条会报错- 读取并返回Map
fetchMap
读取并返回一个MapfetchGroups
读取并返回一个分组Map
fetch
-
fetch()
无参调用此方法,返回的是一个Result
结果集对象Result<Record> records = dslContext.select().from(S1_USER).fetch();
-
fetch(RecordMapper mapper)
RecordMapper
接口的提供map
方法,用于来返回数据。map
方法传入一个Record
对象。可以使用lambda表达式将Record
对象转换成一个指定类型的POJOList<S1UserPojo> userPojoList = dslContext.select() .from(S1_USER) .where(S1_USER.ID.eq(1)) .fetch(r -> r.into(S1UserPojo.class));
多表查询,字段相同时,直接用into方法将结果集转换为POJO时,相同字段名称的方法会以最后一个字段值为准。这时候,我们可以现将结果集通过
into(Table table)
方法将结果集转换为指定表的Record
对象,然后再into
进指定的POJO类中// 多表关联查询,查询s2_user_message.id = 2的数据,直接into的结果getId()却是1 // 这是因为同时关联查询了s1_user表,该表的id字段值为1 List<S2UserMessage> userMessage = dslContext.select().from(S2_USER_MESSAGE) .leftJoin(S1_USER).on(S1_USER.ID.eq(S2_USER_MESSAGE.USER_ID)) .where(S2_USER_MESSAGE.ID.eq(2)) .fetch(r -> r.into(S2UserMessage.class)); // userMessage.getId() == 1 // 将结果集into进指定的表描述中,然后在into至指定的POJO类 List<S2UserMessage> userMessage2 = dslContext.select().from(S2_USER_MESSAGE) .leftJoin(S1_USER).on(S1_USER.ID.eq(S2_USER_MESSAGE.USER_ID)) .where(S2_USER_MESSAGE.ID.eq(2)) .fetch(r -> S2UserMessage fetchUserMessage = r.into(S2_USER_MESSAGE).into(S2UserMessage.class); fetchUserMessage.setUsername(r.get(S1_USER.USERNAME)); return fetchUserMessage; ); // userMessage.getId() == 2
-
fetch(Field field)
Field
是一个接口,代码生成器生成的表字段常量例如S1_USER.ID
, 都实现了Field
接口,这个重载可以直接取出指定表字段,会自动根据传入的字段推测其类型List<Integer> id = dslContext.select().from(S1_USER).where(S1_USER.ID.eq(1)) .fetch(S1_USER.ID);
-
fetch(String fieldName, Class type)
可以直接通过字段名称字符串获取指定字段值,可以通过第二个参数指定返回值,如果不指定,返回ObjectList<Integer> idList = dslContext.select().from(S1_USER).where(S1_USER.ID.eq(1)) .fetch("id", Integer.class);
-
fetch(int fieldIndex, Class type)
可以通过查询字段下标顺序进行查询指定字段,可以通过第二个参数指定返回值,如果不指定,返回ObjectList<Integer> idList = dslContext.select(S1_USER.ID, S1_USER.USERNAME) .from(S1_USER).where(S1_USER.ID.eq(1)).fetch(0, Integer.class);
fetch
此方法可以将结果集处理为一个Map格式,此方法有很多重载,这里介绍几个常用的,注意,此方法作为key的字段必须确定是在当前结果集中是唯一的,如果出现重复key,此方法会抛出异常
-
fetchMap(Field field, Class type)
以表字段值为key,返回一个K:V
的Map对象Map<Integer, S1UserPojo> idUserPojoMap = dslContext.select().from(S1_USER) .fetchMap(S1_USER.ID, S1UserPojo.class);
-
fetchMap(Feild field, Field field)
以表字段值为key,返回一个K:V
的Map对象Map<Integer, String> idUserNameMap = dslContext.select().from(S1_USER) .fetchMap(S1_USER.ID, S1_USER.USERNAME);
fetchgroup
此方法可以将结果集处理为一个Map格式,和fetchMap
类似,只不过这里的值为一个指定类型的集合,通常在处理一对多数据时会用到
-
fetchGroups(Field field, Class type)
以表字段值为Key,返回一个K:List
的Map对象Map<Integer, List<S2UserMessage>> userIdUserMessageMap = dslContext.select().from(S2_USER_MESSAGE) .fetchGroups(S2_USER_MESSAGE.USER_ID, S2UserMessage.class);
-
fetchGroups(Field keyField, Field valueField)
-
以表字段值为Key,返回一个K:List的Map对象
Map<Integer, List<Integer>> userIdUserMessageIdMap = dslContext.select().from(S2_USER_MESSAGE) .fetchGroups(S2_USER_MESSAGE.USER_ID, S2_USER_MESSAGE.ID);
jooq multiset jsonb 列失败,无法构造 `org.jooq.JSONB` 的实例
【中文标题】jooq multiset jsonb 列失败,无法构造 `org.jooq.JSONB` 的实例【英文标题】:jooq multiset jsonb column fails with Cannot construct instance of `org.jooq.JSONB` 【发布时间】:2021-11-30 19:51:14 【问题描述】:我第一次体验了 jooqs 新的多集功能。 有一个产品表,每个产品可以分配可变数量的存储。每个存储可能有一个 storage_coordinate_instance,而一个 storage_coordinate_instance 有一个列层次结构,它保存存储位置的解析递归表示。 (更新非常少,存储层次结构避免了每次需要存储位置时解析递归查询)。
一切都很好,直到我尝试将层次结构列添加到多重集。
这是查询:
List<ProductFilterItem> items =
dslContext
.select(
PRODUCT.ID,
PRODUCT.NAME,
PRODUCT.ARTICLE_NUMBER,
PRODUCT.PHYSICAL,
multiset(
select(
PRODUCT_STORAGE.ID,
PRODUCT_STORAGE.PRODUCT_ID,
PRODUCT_STORAGE.STOCK,
PRODUCT_STORAGE.COORDINATE_INSTANCE_ID,
STORAGE_COORDINATE_INSTANCE.HIERARCHY
)
.from(PRODUCT_STORAGE)
.leftOuterJoin(STORAGE_COORDINATE_INSTANCE).on(STORAGE_COORDINATE_INSTANCE.ID.eq(PRODUCT_STORAGE.COORDINATE_INSTANCE_ID))
.where(PRODUCT_STORAGE.PRODUCT_ID.eq(PRODUCT.ID))
).as("storage").convertFrom(r -> r.into(ProductStorageItem.class))
)
.from(PRODUCT)
.where(queryCondition)
.fetchInto(ProductItem.class)
这就是存储项映射到的类:
private static class ProductStorageItem
private final UUID id;
private final UUID productId;
private final Double stock;
private final UUID coordinateInstanceId;
private final JSONB hierarchy;
public ProductStorageItem(UUID id, UUID productId, Double stock, UUID coordinateInstanceId, JSONB hierarchy)
this.id = id;
this.productId = productId;
this.stock = stock;
this.coordinateInstanceId = coordinateInstanceId;
this.hierarchy = hierarchy;
如果当前过滤未返回分配了 storage_coordinate_instance 的产品,则查询不会出错。
否则会产生
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `org.jooq.JSONB` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)""606f4346-292e-4479-a2a1-39bb49e44873":"2","8d86347f-fc31-4f88-92c4-6e4f5fd12626":"2","d2bbbce0-1487-4f87-b458-07c4e8a54065":"2""; line: 1, column: 2]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548) ~[jackson-databind-2.12.3.jar:2.12.3]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516) ~[jackson-databind-2.12.3.jar:2.12.3]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.jooq.impl.Convert$ConvertAll.from(Convert.java:1139) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.Convert.convert0(Convert.java:426) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.Convert.convert(Convert.java:501) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.AbstractDataType.convert(AbstractDataType.java:538) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultDataType.convert(DefaultDataType.java:97) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.ConvertedDataType.convert(ConvertedDataType.java:224) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.Tools.setValue(Tools.java:3068) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultRecordUnmapper$IterableUnmapper.unmap(DefaultRecordUnmapper.java:189) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultRecordUnmapper.unmap(DefaultRecordUnmapper.java:102) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.AbstractRecord.from0(AbstractRecord.java:911) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.AbstractRecord.from(AbstractRecord.java:941) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.JSONReader.lambda$read$1(JSONReader.java:201) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.RecordDelegate.operate(RecordDelegate.java:143) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.JSONReader.read(JSONReader.java:200) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.JSONReader.read(JSONReader.java:110) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultBinding$DefaultResultBinding.readMultiset(DefaultBinding.java:3829) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultBinding$DefaultResultBinding.get0(DefaultBinding.java:3808) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultBinding$DefaultResultBinding.get0(DefaultBinding.java:3786) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.DefaultBinding$AbstractBinding.get(DefaultBinding.java:946) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(CursorImpl.java:1551) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.CursorImpl$CursorIterator$CursorRecordInitialiser.apply(CursorImpl.java:1500) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.CursorImpl$CursorIterator$CursorRecordInitialiser.apply(CursorImpl.java:1459) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.RecordDelegate.operate(RecordDelegate.java:143) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.CursorImpl$CursorIterator.fetchNext(CursorImpl.java:1424) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.CursorImpl$CursorIterator.hasNext(CursorImpl.java:1400) ~[jooq-3.15.3.jar:na]
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:132) ~[na:na]
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[na:na]
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[na:na]
at org.jooq.impl.AbstractCursor.collect(AbstractCursor.java:78) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.ResultQueryTrait.collect(ResultQueryTrait.java:358) ~[jooq-3.15.3.jar:na]
at org.jooq.impl.ResultQueryTrait.fetchInto(ResultQueryTrait.java:1423) ~[jooq-3.15.3.jar:na]
at package.MyProductDao.filterProducts(MyProductDao.java:129) ~[classes/:na]
查询有问题吗?或者这可能是与 jooq 基于 JSON 的 postgres 多集仿真有关的问题?
Java 11 Jooq 3.15.3 Postgres 驱动 42.2.24
感谢您的帮助!
亲切的问候, 安德烈亚斯
小编辑:如何按多集的大小对结果集进行排序?无论是实际大小,还是伪大小:将至少有一个存储的放在前面,然后将没有的放在后面。
【问题讨论】:
好的,关于通过多重集对结果集进行排序,可以使用orderBy(inline(5).desc())
。这给出了预期的结果,即分配了 product_storage 的那些被放在第一位。如果有更多的选择会很有趣。
我建议单独问一个关于排序的问题,因为它与错误没有严格的关系。我们可以在那里讨论订购选项...
感谢您的回复。我在这里发布了另一个问题***.com/questions/69552492/… 好的,您假设内部错误导致查询失败?
【参考方案1】:
jOOQ 3.15.3 中似乎存在一个错误,它试图通过 Jackson 将 JSON
数据映射到 JSON
或 JSONB
,而不是在内部通过 jOOQ 自己的序列化器,请参阅:
DefaultConverterProvider
提供这种映射的能力)
该错误将在 3.16.0 和 3.15.4 中修复。作为一种解决方法,您可以暂时将您的 JSONB
文档转换为 TEXT
。
【讨论】:
感谢您的回复和提示,通过STORAGE_COORDINATE_INSTANCE.HIERARCHY.cast(String.class)
选择允许获取层次结构!以上是关于JOOQ用法和实例的主要内容,如果未能解决你的问题,请参考以下文章
使用 JOOQ 在 java.sql.Timestamp 和 java.time.Instant 之间转换时遇到问题