摘要:MongoDB学习过程中对CRUD用法的记录,毕竟好记性不如烂笔头。
文章说明
文章作者:鴻塵
文章链接:https://hwame.top/20210716/mongodb-crud-operations.html
参考资料:MongoDB CRUD Operations: Version 5.0 latest
1.概述
1.1.写作的动机
由于工作需要,捡起了遗忘多时的MongoDB,项目中使用的是4.2版本,恰好本地电脑上多年前装的也是4.2版本,如今官方文档都已经更新到5.0了。
相比旧版本,5.0增加了一些新功能,比如聚合里的一些stage,4.2版本算是一个比较经典的版本吧(就好比mysql的5.7和8.0)。
决定记录的另一个动机,则是网上的教程不够全面,官方文档也没有中文版的,有些翻译很生硬且不准确:
- 书栈网
- 英文版教程MongoDB v4.2 Manual,与官方文档无异。
- MongoDB (v3.4) 中文手册翻译不全,很多章节仍然是英文。
- MongoDB入门指南和MongoDB学习总结过于简洁、不够全面。
- MongoDB数据库最佳实践更侧重于实际应用,适合于进阶实战。
- 菜鸟教程的MongoDB教程相比上面几个要好得多,但是毕竟偏向入门,很多细节没有展开。
在比较之后遂决定啃最新的5.0官方文档,怎么说呢,任何问题都可以从中找到答案。
MongoDB提供了一个在线版的Mongo shell,默认为
latest
即v5.0,可以通过参数选择版本,例如https://mws.mongodb.com/?version=4.2。
唯一的缺点就是连接维持的时间比较短,一不留神就断开了,重连会丢失之前的数据。
1.2.啃文档的一些感受
这篇文章写的是CRUD,是相对比较基础的部分,目前在看管道聚合,准备写下一篇。
在看过几天文档(尤其是管道聚合部分)后有一些感受,主要来自管道聚合,但是估计下一篇内容会很多,这篇内容较少就放在这里吧。主要有以下几点:
- ①官方文档写的很细,例如Aggregation Pipeline Stages和Aggregation Pipeline Operators都有名为
$count
的累积器accumulator和管道阶段stage,Aggregation Pipeline Operators里有分别名为$first
和$last
的同名累积器accumulator和操作符operator。文档不厌其烦地在每个出现的地方都有「消除歧义Disambiguation」的提示,但这也导致了文档比较冗杂。 - ②官方文档内容比较混乱,从路由上就可以看出来。比如Aggregation Pipeline Stages和Aggregation Pipeline Operators路由的命名分别为
aggregation-pipeline
和aggregation
,但是这两个对应的类似$xxx
的格式却都是https://docs.mongodb.com/manual/reference/operator/aggregation/xxx/
,这难免让人摸不着头脑。为何不将路由命名为aggregation-pipeline-stages
和aggregation-pipeline-operators
,且将对应$xxx
放到各自路径下呢? - ③官方文档内容分类不够具体,如果能按分类添加下级路由,那么文档的结构和层次会更清楚了。如下两例:
- 例如,Aggregation Pipeline Stages可分为「
db.collection.aggregate()
Stages」、「db.aggregate()
Stages」和「Stages Available for Updates」三个部分,完全可以添加三个子路由啊。 - 还有,Aggregation Pipeline Operators操作表达式分为「算术表达式操作符」、「数组表达式操作符」、「布尔表达式操作符」、「比较表达式操作符」、「条件表达式操作符」、「自定义聚合表达式操作符」、「数据尺寸操作符」、「日期表达式操作符」、「字面表达式操作符」、「杂项操作符」、「对象表达式操作符」、「
set
表达式操作符」、「字符串表达式操作符」、「文本表达式操作符」、「三角函数表达式操作符」、「类型表达式操作符」、「$group
阶段的累积器」、「非$group
阶段的累积器」、「变量表达式操作符」和「窗口操作符」。单单是分类就已经有20个之多,内容那就更多了。如果按分类添加20个子路由,文档的结构和层次会更清楚了。 - 当然,文档很贴心地在Aggregation Pipeline Stages和Aggregation Pipeline Operators最后添加了按字母排序的列表(Alphabetical Listing)以供索引查阅。需要说明的是,各分类并不是互斥的,各类里面有重叠的项,大概这就是官方不予添加子路由的原因吧。
- 例如,Aggregation Pipeline Stages可分为「
- ④相近的概念没有辨析清楚【这里应该与翻译和表达有关,不全是官方的锅】,比如「管道聚合」里的stage和operation是并列的,operator用于stage/operation里,中文里「操作」一词即可作名词也可做动词,翻译阅读起来就会产生歧义,所以我把stage和operation统一当成「阶段」,operator则称为「操作符」或「操作表达式」。从这个层面上说,官方文档的描述还是很准确的,只不过全是长难句一连串的定语有时候都分不清修饰的到底是谁,引起理解上的歧义。
我们常说的CRUD即是「增删改查」,具体说来:
C
=Create
,增,即创建操作;R
=Read
,查,即查询操作;U
=Update
,改,即更新操作;D
=Delete
,删,即删除操作。
2.创建操作
创建/插入是针对单个集合而言,「写操作」在单个文档级别上具有「原子性」。如果当前集合不存在,则插入操作将创建该集合。1
2
3
4# 插入单个文档是「{}」,多个是「[{}, {}, ...]」
db.collection.insertOne() # 单个
db.collection.insertMany() # 多个
db.collection.insert() # 单个或多个
与upsert: true
选项 一起使用时也可实现插入:
db.collection.update()
,db.collection.updateOne()
,db.collection.updateMany()
;db.collection.findAndModify()
,db.collection.findOneAndUpdate()
,db.collection.findOneAndReplace()
;db.collection.bulkWrite()
:Performs multiple write operations with controls for order of execution。
3.查询操作
查询操作语法为db.collection.find(query, projection)
,其中query
指定查询条件,projection
指定返回的字段。
以下示例使用inventory
集合,包含如下数据:1
2
3
4
5{ item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" }
{ item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" }
{ item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" }
{ item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" }
{ item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }
- 选择集合中的所有文档:
query={}
或query=
(留空不写),相当于SELECT * FROM inventory
。 - 指定相等条件:
query={<field1>: <valve1>, ...}
,例如{status: "D"}
相当于SELECT * FROM inventory WHERE status = "D"
。 - 使用查询运算符(Query Operators)指定条件:
{<field1>: {<operator1>: <value1>}, ...}
,例如{status: {$in: ["A", "D"]}}
相当于SELECT * FROM inventory WHERE status in ("A", "D")
【此处也可用$or
,但对同一字段应该用$in
而非$or
】。 - 指定
AND
条件:{<and1>: <value1>, <and2>: <value2>, ...}
,例如{status: "A", qty: {$lt: 30}}
相当于SELECT * FRON inventory WHERE status = "A" AND qty < 30
。 - 指定
OR
条件:{$or: [<query1>, <query2>, ...]}
,例如{$or: [{status: “A"}, {qty: {$lt: 30}}]}
相当于SELECT * FROM inventory WHERE status = "A" OR qty < 30
。 - 指定
AND
以及OR
条件【即4+5
组含】:<and>: <value>, $or: [<query1>, <query2>, ...]
,例如{status: "A", $or: [qty: {$lt: 30}}, {item: /^p/}]}
相当于SELECT * FROM inventory WHERE status = "A" AND (qty < 30 OR item LIKE "p%")
【支持正则表达式】。
3.1.查询嵌入/嵌套文档
- 嵌套文档匹配即
<value = document>
,格式为:query={<field1>: <doc1>, ...}
,例如{size: {h: 14, w: 21,uom: "cm"}}
;注意: 整个嵌入文档的相等匹配需要精确匹配,包括顺序! - 嵌套字段匹配即嵌套文挡中字殿的匹配,字段使用点表示法:
field.subField
。- 相等匹配:
{<field.subField>: <value>, ...}
,例如{"size.uom": "in"}
; - 查询运算符:
{<field1>: {<operator1>: <value1>}, ...}
,例如{"size.h": {$lt: 15}}
,与 3. 相同; - 指定
AND
条件:{<field1.subField1>: <valuel>, ...}
,例加{"size.h": {$lt: 15}, "size.uom": "in", states: "D"}
,与 4. 相同。
- 相等匹配:
3.2.查询数组
- 匹配一个数组,即
value = array
,精确匹配包括顺序。- 例如
{tags: ["red", "black"]}
只匹配包含2个元素的定序的["red", "black"]
; - 若要匹配无序的包含此2元素的数组(
len ≥ 2
),则需使用$all
运算符:{tags: {$all: ["red", "black"]}}
。
- 例如
- 查询一个元素的数组(包含指定元素的数组),即
arrField = arrValue
。- 例如
{tags: "red"}
匹配"red" in array(tags)
; - 可以对
arrValue
指定条件过滤器,即{arrField1: {operator1: value1, ...}}
,例如{dim_cm: {$gt: 25}}
匹配any(dim_cm[i] > 25)
,即dim_cm
中存在大于25的元素。
- 例如
- 为数组元素指定多个条件(复合条件),使 单个数组元素 满足这些条件或 数组元素的任意组合 满足条件。
- 在数组元素上使用复合过滤条件查询数组:数组中存在满足条件的元素(这些元素同时存在于该数组即可,无需是同一个元素),例如
{dim_cm: {Sgt: 15, slt: 20}}
表示dim_cm[i] > 15 AND dim_cm[j] < 20
或15 < dim_cm[kJ < 20
; - 查询满足多个条件的数组元素:一个元素同时满足条件,例如
{dim_cm: {$elemMatch: {Sgt: 15, $lt: 20}}}
表示15 < dim_cm[i] < 20
; - 通过数组索引位置查询元素:点表示法(dot notation)可以为指定位置的元素设置条件,索引从0开始【 字段和嵌套字段必须在引号内 】,例如
{"dim_cm.1": {$gt: 25}}
表示dim_cm[1] > 25
; - 按数组长度查询数组:
{"tags": {$size: 3}}
表示len(tags) = 3
。
- 在数组元素上使用复合过滤条件查询数组:数组中存在满足条件的元素(这些元素同时存在于该数组即可,无需是同一个元素),例如
3.3.查询嵌套文档数组
嵌套文档数组(Array of Embedded Documents)即以文档为元素的数组,集合
inventory
文档格式为{item: "xx", instock: [{warehouse: "A", qty: 5}, {}...]}
。
- 查询嵌套在数组中的文档:
{field: doc}
即doc in array(field)
(查询数组2:包含指定元素的数组),doc
精确匹配包括顺序。例如{"instock": {warehouse: "A", qty: 5}}
匹配的数组instock
包含整个「有序」文档{warehouse: "A", qty: 5}
。 - 在文档数组中的字段上指定查询条件【点表示法 字段和嵌套字段必须在引号内 】。
- 对嵌入在文档数组中的字段指定查询条件:例如
{'instock.qty': {$lte: 20}}
匹配数组元素instock[i] = doc,doc.qty ≤ 20
,不检查doc
位置; - 使用数组索引查询嵌入文档中的字段: 例如
{'instock.0.qty': {$lte: 20}}
匹配数组元素instock[0] = doc,doc.qty ≤ 20
,指定doc
位置为0,因此结果是上一个的 子集 。
- 对嵌入在文档数组中的字段指定查询条件:例如
- 为文档数组指定多个条件,使得数组中的文档或文档组合满足条件。
- 单个嵌套文档在嵌套字段上满足多个查询条件,例如
{"instock": {$elemMatch: {qty: 5, warehouse: "A"}}}
匹配instock[i]=doc,{qty: 5, warehouse: "A"} in doc
,此处doc
包含这两值即可且无序。再如{"instock": {$elemMatch: {qty: {$gt: 10, $lte: 20}}}}
匹配instock[i]=doc,10 < doc.qty < 20
; - 元素组合满足条件, 若数组字段上的复合查询条件不使用
$elemMatch
运算行,则匹配「文档组合满足条件的数组」,注意是 文档组合 满足而非 文档 满足。例如{"instock.qty": {$gt: 10, $lte: 20}}
匹配instock[i].qty > 10,instock[j].qty < 20
【上一个是单个文档同时满足,使用了$elemMatch
】。类似的,{"instock.qty": 5, "instock.warehouse": "A"}
将匹配instock[i].qty = 5,instock[j].warehouse = "A"
而不要求位于同一文档中。
- 单个嵌套文档在嵌套字段上满足多个查询条件,例如
3.4.从查询返回指定字段
从查询返回指定字段即投影(Projection),查询语法
db.inventory.find(query, projection)
,其中projection.field = (1=true | 0=false)
。
查询默认返回所有字段【相当于SELECT *
】,使用投影(Projection)来指定或限制返回的字段【相当于SELECT field1, field2...
】。
- 返回匹配文档中的所有字段:不指定
projection
相当于SELECT *
。 - 仅返回指定字段和
_id
:projection = {field1: 1, field2: 1}
,默认会返回_id
字段,相当于SELECT _id, field1, field2
。 - 禁用
_id
字段,通过 显式地置0 从结果中删除_id
字段:projection = {field1: 1, field2: 1, _id: 0}
。 - 返回除排除字段之外的所有字段(补集思想),通过置0来排除字段,返回剩下的字段。 注意: 除了
_id
字段以外,不能在projection
文档中对「包含和排除语句」进行组合,即不能同时出现0和1。 - 返回/禁用嵌套文档中的特定字段:使用点表示法指定嵌套文档中的特定字段,如
projection = {item: 1, status: 1, "size.uom": 1}
、projection = {"size.uom": 0}
。 - 数组中嵌入文档的投影,使用点表示法在嵌入数组的文档中投影特定字段,例如
projection = {item: 1, status: 1, "instock.qty": 1}
的查询结果中instock
数组元素(即doc
文档)只有qty
字段。 - 在返回的数组中投影指定数组元素:对于包含了数组的字段,投影算子用来操作数组。例如
projection = {item: 1, status: 1, instock: {$slice: -1}}
表示只取instock
数组最后一个元素。上述3个投影算子是「从返回数组中」投影「指定元素」的 唯一 方法。也就是说,不能使用素引
{"instock.0": 1}
,将报错SyntaxErrorexpected property name, got '{'
。
3.5.查询空字段或缺失字段
MongoDB中的不同查询运算符对null
值的处理方式不同。查询语法db.inventory.find(query, projection)
,示例集合inventory
为:1
2{ _id: 1, item: null}
{ _id: 2}
- 相等过滤器:
query = {item: null}
查询匹配item = null OR !item
,结果返回所有的两个文档。 - 类型检查:
query = {item: {$type: 10}}
查询匹配item = null
即字段item
的值为「BSON类型值Null
,其类型编号为10」,结果返回_id = 1
的文档。 - 存在检查,
$exists
检查是否存在指定字段:query = {item: {$exists: false}}
查询匹配!item
即exists(item) = false
,结果返回_id = 2
的文档(不包含item
字段)。
4.更新操作
更新操作用于修改集合中已有的文档,在单个文档级别上具有「原子性」,(与「增」相同)。
可以指定条件或过滤器,来进行更新:1
2
3db.collection.updateOne(<filter>, <update>, <options>)
db.collection.updateMany(<filter>, <update>, <options>)
db.collection.replaceOne(<filter>, <update>, <options>)
利用更新操作符如$set
来修改字段值,「更新方法」的格式:
注意:如果字段不存在,某些更新运算符(例如$set
)将创建该字段。1
2
3
4{
<update_operator1>: {<field1>: <value1>, ...},
<update_operator2>: {<field2>: <value2>, ...},
}
例1:更新单个文档
例如原文档:{item: "paper", qty: 100, size: {h: 8.5, w: 11, uom: "in"}, status: "D"}
;
更新后文档:{item: "paper", qty: 100, size: {h: 8.5, w: 11, uom: "cm"}, status: "P", lastModified: ISODate("2021-07-21T02:40:43.515Z")}
。
更新操作:
- 使用
$set
修改"size.uom" = "cm"
和status = "p"
; - 使用
$currentDate
将lastModified
更新为当前日期,不存在的字段将被创建。
1 | db.inventory.updateOne( |
例2:更新多个文档
1 | # 例如原文档 |
更新操作:
- 更新满足
qty < 50
的所有文档(例1中qty = 100
); - 使用
$set
修改"size.uom" = "in"
和status = "p"
; - 使用
$currentDate
将lastModified
更新为当前日期,不存在的字段将被创建。
1 | db.inventory.updateMany( |
例3:替换单个文档
例如文档的更新情况:
- 原始示例文档:
{item: "paper", qty: 100, size: {h: 8.5, w: 11, uom: "in"}, status: "D"}
; - 例1修改后文档:
{item: "paper", qty: 100, size: {h: 8.5, w: 11, uom: "cm"}, status: "P", lastModified: ISODate("2021-07-21T02:40:43.515Z")}
; - 例3修改后文档:
{item: "paper", instock: [{warehouse: "A", qty: 60}, {warehouse: "B", qty: 40}]}
。
更新操作,直接修改整个item
字段为paper
的文档:1
2
3
4db.inventory.replaceOne(
{item: "paper"},
{item: "paper", instock: [{warehouse: "A", qty: 60}, {warehouse: "B", qty: 40}]}
)
使用聚合管道更新
从MongoDB 4.2开始,更新操作可以使用聚合管道(Aggregation Pipeline),聚合管道可以由以下stage组成:$addFields
、$set
、$project
、$unset
、$replaceRoot
和$repaceWith
。
使用聚合管道允许更具表现力的update
语句,例如基于当前字段值来表示条件更新,或者使用另一个字段的值更新一个字段。
- 示例1。
db.students.updateOne({_id: 3}, [{$set: {"test3": 98,modified: "$$NOW"}}])
更新文档_id: 3
,其中$set
stage将创建不存在的字段test3 = 98
,并将字段modified
更新为当前时间。该操作使用「聚合变量(aggregation variable)」NOW
获取当前时间,使用「双dollar符前缀」并「加引号」来访问变量:"$$NOW"
。 - 示例2。使用聚合管道来标准化文档中的字段,即集合中的文档应具有相同的字段,同时更新
modified
字段。其中:- 带有
$mergeObjects
表达式的$replaceRoot
stage用来为quiz1
、quiz2
、test1
和test2
字段设置默认值,聚合变量ROOT
指的是当前正在修改的文档。当前文档字段将覆盖默认值。 $set
stage将modified
字段更新为当前时间。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18db.students2.insertMany([
{"_id": 1, quiz1: 8, test2: 100, quiz2: 9, modified: new Date("01/05/2020")},
{"_id": 2, quiz2: 5, test1: 80, test2: 89, modified: new Date("01/05/2020")},
])
db.students2.updateMany({},
[
{$replaceRoot: {newRoot:
{$mergeObjects: [{quiz1: 0, quiz2: 0, test1: 0, test2: 0}, "$$ROOT"]}
}
},
{$set: {modified: "$$NOW"}}
]
)
# 结果:
# {_id: 1, quiz1: 8, quiz2: 9, test1: 0, test2: 100, modified: ISODate("2021-07-21T04:03:43.185Z")},
# {_id: 2, quiz1: 0, quiz2: 5, test1: 80, test2: 89, modified: ISODate("2021-07-21T04:03:43.185Z")}
- 带有
- 示例3。使用聚合管道来计算「平均分数和成绩等级」,同时更新
modified
字段。其中:- 第一个
$set
stage用来①计算tests
数组的平均值({$avg: "$tests"}
)并截断取整({$trunc: [<number>, 0]}
),将取整的值赋给average
并创建;②将modified
字段更新为当前时间。【$trunc用法】 - 第二个
$set
stage根据上一步的average
,使用$switch
表达式创建grade
。【$switch用法】1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25db.students3.insert([
{"_id": 1, "tests": [95, 92, 98], "modified": ISODate("2019-01-01T00:00:00Z")},
{"_id": 2, "tests": [94, 88, 90], "modified": ISODate("2019-01-01T00:00:00Z")},
{"_id": 3, "tests": [70, 75, 82], "modified": ISODate("2019-01-01T00:00:00Z")}
]);
db.students3.updateMany({},
[
{$set: {average: {$trunc [{$avg: "$tests"}, 0]}, modified: "$$NOW"}},
{$set: {grade: {$switch: {
branches: [
{case: {$gte: ["$average", 90]}, then: "A"},
{case: {$gte: ["$average", 80]}, then: "B"},
{case: {$gte: ["$average", 70]}, then: "C"},
{case: {$gte: ["$average", 60]}, then: "D"}
],
default: "F"
}}}}
]
)
# 结果:
# {"_id": 1, "tests": [95, 92, 90], "modified": ISODate("2021-07-21T06:06:50.533Z"), "average": 92, "grade": "A"},
# {"_id": 2, "tests": [94, 88, 90], "modified": ISODate("2021-07-21T06:06:50.533Z"), "average": 90, "grade": "A"},
# {"_id": 3, "tests": [70, 75, 82], "modified": ISODate("2021-07-21T06:06:50.533Z"), "average": 75, "grade": "C"},
- 第一个
- 示例4。使用聚合管道进行array的拼接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16db.students4.insertMany([
{"_id": 1, "quizzes": [4, 6, 7]},
{"_id": 2, "quizzes": [5]},
{"_id": 3, "quizzes": [10, 10, 10]}
])
db.students4.updateOne({_id: 2},
[
{$set: {quizzes: {$concatArrays: ["$quizzes", [8, 6]]}}}
]
)
# 结果:
# {"_id": 1, "quizzes": [4, 6, 7]},
# {"_id": 2, "quizzes": [5, 8, 6]},
# {"_id": 3, "quizzes": [10, 10, 10]} - 示例5。使用聚合管道将「摄氏温度」转换为「华氏温度」,其中:
- 管道包含
$addFields
stage(与$set
等价),用以创建新的数组字段tempsF
(包含华氏温度的数组); - 该stage使用
$map``$add
和$multiply
表达式来进行温度转换$F=C\times \frac{9}{5}+32$。【$map用法,对len(tempsC)
无要求】1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22db.temperatures.insertMany([
{"_id": 1, "date": ISODate("2019-06-23"), "tempsC": [ 4, 12, 17]},
{"_id": 2, "date": ISODate("2019-07-07"), "tempsC": [14, 24, 11]},
{"_id": 3, "date": ISODate("2019-10-30"), "tempsC": [18, 6, 8]}
])
db.temperatures.updateMany({},
[
{$addFields: {"tempsF": {
$map: {
input: "$tempsC",
as: "celsius",
in: {$add: [{$multiply: ["$$celsius", 9/5]}, 32]}
}
}}}
]
)
# 结果:
# {"_id": 1, "date": ISODate("2019-06-23T00:00:00.000Z"), "tempsC": [ 4, 12, 17], "tempsF": [39.2, 53.6, 62.6]},
# {"_id": 2, "date": ISODate("2019-07-07T00:00:00.000Z"), "tempsC": [14, 24, 11], "tempsF": [57.2, 75.2, 51.8]},
# {"_id": 3, "date": ISODate("2019-10-30T00:00:00.000Z"), "tempsC": [18, 6, 8], "tempsF": [64.4, 42.8, 46.4]}
- 管道包含
update方法
1 | db.collection.updateOne() |
5.删除操作
从集合中删除文档,具有「原子性」,也可指定条件或过滤器:1
2db.collection.deleteOne(filter)
db.collection.deleteMany(filter)
- 删除所有文档。将过滤器文档置空:
db.inventory.deleteMany({})
。 - 删除所有符合条件的文档。指定用于标识要删除文档的条件或过滤器,过滤器语法与查询操作相同。在「查询过滤器文档(query filter docuent)」中使用
field: value
表达式来指定相等匹配条件:{<field1>: <value1>, ...}
,同理,将value
替换成「查询操作符」来指定匹配条件:{<field1>: {<operator1>: <value1>}, ...}
。 - 只删除一个符合条件的文档。即使多个文档相匹配,也只删除第一个:
db.collection.deleteOne(filter)
。 - 删除方法:
db.collection.deleteOne()
;db.collection.deleteMany()
;db.collection.remove()
;db.collection.findOneAndDelete()
;db.collection.findAndModify()
;db.collection.bulkWrite()
。