MongoDB
Table of Contents
1. MongoDB 简介
MongoDB is a document-oriented NoSQL database.
1.1. MongoDB VS. CouchDB
MongoDB 与 CouchDB 很相似,它们都是文档型存储,数据存储格式都是 JSON 型的。
MongoDB 和 CouchDB 的一个重要区别:CouchDB 是一个 MVCC 的系统,而 MongoDB 是一个 update-in-place 的系统。这二者的区别就是,MongoDB 进行写操作时都是即时完成写操作,写操作成功则数据就写成功了;而 CouchDB 一个支持多版本控制的系统,此类系统通常支持多个节点写,而系统会检测到多个系统的写操作之间的冲突并以一定的算法规则予以解决。
1.2. MongoDB 和 RDBMS 的概念对应关系
MongoDB 和 RDBMS 的概念对应关系及其不同如表 1 所示。
RDBMS | MongoDB | Difference |
---|---|---|
Table | Collection | In RDBMS, the table contains the columns and rows which are used to store the data whereas, in MongoDB, this same structure is known as a collection. The collection contains documents which in turn contains Fields, which in turn are key-value pairs. |
Row | Document | In RDBMS, the row represents a single, implicitly structured data item in a table. In MongoDB, the data is stored in documents. |
Column | Field | In RDBMS, the column denotes a set of data values. These in MongoDB are known as Fields. |
Joins | Embedded documents | In RDBMS, data is sometimes spread across various tables and in order to show a complete view of all data, a join is sometimes formed across tables to get the data. In MongoDB, the data is normally stored in a single collection, but separated by using Embedded documents. So there is no concept of joins in Mongodb. |
1.3. 启动服务器
下面命令可启动 MongoDB 服务器:
$ mongod --config /usr/local/etc/mongod.conf $ mongod --config --fork /usr/local/etc/mongod.conf # --fork 表示以 daemon 形式启动
MongoDB 服务器默认监听在端口 27017,可以通过 --port
参数修改监听端口。
下面是配置文件 /usr/local/etc/mongod.conf 的一个样例:
$ cat /usr/local/etc/mongod.conf systemLog: destination: file path: /usr/local/var/log/mongodb/mongo.log logAppend: true storage: dbPath: /usr/local/var/mongodb net: bindIp: 127.0.0.1
1.4. 启动客户端
直接运行 mongo
可启动客户端,默认连接本机的 27017 端口,执行 help 可以查看在线帮助文档。
$ mongo MongoDB shell version v4.0.0 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 4.0.0 > help db.help() help on db methods db.mycoll.help() help on collection methods sh.help() sharding helpers rs.help() replica set helpers help admin administrative help help connect connecting to a db help help keys key shortcuts help misc misc things to know help mr mapreduce show dbs show database names show collections show collections in current database show users show users in current database show profile show most recent system.profile entries with time >= 1ms show logs show the accessible logger names show log [name] prints out the last segment of log in memory, 'global' is default use <db_name> set current database db.foo.find() list objects in collection foo db.foo.find( { a : 1 } ) list objects in foo where a == 1 it result of the last line evaluated; use to further iterate DBQuery.shellBatchSize = x set default number of items to display on shell exit quit the mongo shell
可以使用参数 --host
和 --port
来改变连接的服务器地址。如不带参数直接执行 mongo
相当于执行:
mongo --host 127.0.0.1 --port 27017
2. MongoDB 基本操作
2.1. 创建、查看、删除数据库
使用 show dbs
可查看系统中有哪些数据库。如:
> show dbs admin 0.000GB config 0.000GB local 0.000GB
使用 use YOUR_DBNAME
可以切换或创建数据为数据库。如:
> use test1 switched to db test1
执行上面命令后,如果数据库 test1 不存在会创建它,否则切换到数据库 test1。
注:使用 show dbs
不会显示刚刚创建的新数据库,你需要往数据库中插入数据后才会显示。
使用 db
可以查看当前数据库。如:
> db test1
使用 db.dropDatabase()
可以删除当前数据库。如删除 test1 数据库:
> db.dropDatabase() { "dropped" : "test1", "ok" : 1 }
2.2. 创建、查看、删除 collection (table)
可以使用 db.createCollection 来创建 collection。如:
> db.createCollection("people") { "ok" : 1 }
一般你不用使用 createCollect 显式地创建 collection,直接插入文档时会自动创建 collection。如:
> db.testabc.insert({"name" : "cig01"}) // 如果testabc是不存在的collection,则会先创建它 WriteResult({ "nInserted" : 1 })
使用 show collections
(或者 show tables
)可以显示当前数据库下有哪些 collection。如:
> show collections people testabc
使用 db.testabc.drop()
可以删除集合 testabc。如:
> db.testabc.drop() true
2.3. 文档基本操作
2.3.1. 插入文档
使用 db.COLLECTION_NAME.insert()
可以往当前数据库中集合 COLLECTION_NAME 下插入一个或多个文档。如插入一个文档:
> db.products.insert( { item: "card", qty: 15 } ) WriteResult({ "nInserted" : 1 })
如果插入的文档数据中没有 _id
字段,则会自动生成一个 _id
字段;如果插入的文档中带有 _id
字段,则用户自己需要确保 _id
字段不重复。
下面是一次插入多个文档的例子:
> db.products.insert([ { item: "pencil", qty: 15 }, { item: "book", qty: 10} ] ) BulkWriteResult({ "writeErrors" : [ ], "writeConcernErrors" : [ ], "nInserted" : 2, "nUpserted" : 0, "nMatched" : 0, "nModified" : 0, "nRemoved" : 0, "upserted" : [ ] })
使用 db.COLLECTION_NAME.insertOne()
和 db.COLLECTION_NAME.insertMany()
可以往集合中分别插入一个和多个文档,执行成功后它们的返回数据类型和 db.COLLECTION_NAME.insert()
不一样。下面是使用 insertMany()
插入多个文档的实例:
> db.products.insertMany([ { item: "pencil", qty: 30 }, { item: "book", qty: 40} ] ) { "acknowledged" : true, "insertedIds" : [ ObjectId("5b6e7d9490789f89f897931d"), ObjectId("5b6e7d9490789f89f897931e") ] }
可见, insertMany()
的返回数据确实和 insert()
不一样。
注:在 nodejs 的 MongoDB 驱动中 insert()
已经被标记为过时的,推荐使用 insertOne()
和 insertMany()
。
2.3.2. 查询文档
使用 db.COLLECTION_NAME.find()
可以得到集合中的所有文档。如:
> db.products.find() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
通过指定 pretty()
方法可以更好地显示结果。如:
> db.products.find().pretty() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
2.3.2.1. 按条件查询
通过 find()
的参数指定条件,可以查询指定要求的文档,如表 2 所示。
操作 | 格式 | MongoDB 范例 | RDBMS 类似语句 |
---|---|---|---|
等于 | {<key>:<value>} | db.collection.find({"field1":"value1"}) | WHERE field1 = 'value1' |
小于 | {<key>:{$lt:<value>}} | db.collection.find({"field2":{$lt:30}}) | WHERE field2 < 30 |
小于或等于 | {<key>:{$lte:<value>}} | db.collection.find({"field2":{$lte:30}}) | WHERE field2 <= 30 |
大于 | {<key>:{$gt:<value>}} | db.collection.find({"field2":{$gt:30}}) | WHERE field2 > 30 |
大于或等于 | {<key>:{$gte:<value>}} | db.collection.find({"field2":{$gte:30}}) | WHERE field2 >= 30 |
不等于 | {<key>:{$ne:<value>}} | db.collection.find({"field2":{$ne:30}}) | WHERE field2 != 30 |
比如,查询 item 为 book 的文档:
> db.products.find({item: "book"}) { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
又如,查询 qty 大于等于 30 的文档:
> db.products.find({qty: {$gte: 30}}) { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
2.3.2.2. 组合查询(AND 和 OR)
find()
方法可以传入多个 key,每个 key 以逗号隔开,即相当于 RDBMS 中的 AND 条件。如查询 item 为 book,并且 qty 大于等于 30 的文档:
> db.products.find({item: "book", qty: {$gte: 30}}) // AND实例 { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
OR 条件语句使用了关键字 $or
,其语法为:
db.collection.find( { $or: [ {key1: value1}, {key2:value2} ] } )
下面是查询 item 为 book,或者 qty 大于等于 30 的文档:
> db.products.find({$or: [{item: "book"}, {qty: {$gte: 30}} ] }) // OR实例 { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
2.3.2.3. 过滤某些字段
查询文档时,默认会返回所有字段。
我们可以通过 db.collection.find(query, projection)
的第二个参数 projection
来指定返回或不返回哪些字段。它有两种使用方式。
方式一(称为 inclusion 模式):设置需要返回的字段为 1 或者 true,那么其它字段都不会返回。
> db.products.find() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 } > db.products.find({}, {_id: 1, item: 1} ) // inclusion模式 { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card" } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil" } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book" } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil" } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book" }
方式二(称为 exclusion 模式):设置不需要返回的字段为 0 或者 false,那么其它字段都会返回。
> db.products.find({}, {qty: 0}) // exclusion模式 { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card" } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil" } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book" } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil" } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book" }
注:在设置 projection 参数时,我们要么把想要返回的字段都指定为 1(或 true);要么把不想要返回的字段都指定为 0(或 false);我们不能指定部分字段为 0,部分字段为 1。
2.3.3. 更新文档
使用 db.collection.update()
和 db.collection.save()
可以使用更新文档。
update()
的用法如下:
db.collection.update( <query>, <update>, { upsert: <boolean>, multi: <boolean>, // 默认只更新找到的第一个文档,如果这个参数为true,则更新所有文档 writeConcern: <document>, // 设置抛出异常的级别 collation: <document>, arrayFilters: [ <filterdocument1>, ... ] } )
其中:query 用来指定查询条件,而 update 指定更新的对象和一些更新操作符(如 $set
, $inc
等,可参考 https://docs.mongodb.com/manual/reference/operator/update/ )。
例如,更新 item 为 book 的文档,把其 qty 字段设置为 100:
> db.products.update({item: "book"}, {$set : {qty: 100}}) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) > db.products.find() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 100 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
上面例子中,只会更新第一个找到的文档,要更新所有找到的文档,则需要指定 multi
为 true
,如:
> db.products.update({item: "book"}, {$set : {qty: 100}}, {multi:true}) // 所有book的qty设置为100 WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 1 }) > db.products.update({item: "book"}, {$inc : {qty: 1}}, {multi:true}) // 所有book的qty增加1 WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 2 })
save()
方法通过传入的文档来替换已有文档。不指定 _id
字段 save()
方法类似于 insert()
方法;如果指定 _id
字段,则会更新该 _id
的数据。例如,替换 _id
为 5b6e7d9490789f89f897931e 的文档:
> db.products.save({ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 }) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
2.3.4. 删除文档
使用 db.collection.remove()
可以删除指定文档,用法如下:
db.collection.remove( <query>, { justOne: <boolean>, // 是否只删除第一个匹配文档,默认为false writeConcern: <document>, collation: <document> } )
下面是删除 item 为 pencil 的文档的实例:
> db.products.find() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 101 } { "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 } > db.products.remove({item: "pencil"}) // 删除item为pencil的文档 WriteResult({ "nRemoved" : 2 }) > db.products.find() { "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 } { "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 101 } { "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 }
在执行 remove()
命令前先执行 find()
命令来判断执行的条件是否正确,以避免误删数据。
2.4. 数据备份(mongodump/mongorestore)
使用工具 mongodump 和 mongorestore 可以备份和恢复数据库。
备份 mongodb 数据库:
$ mongodump -h <hostname>:<port> -u <user> -p <password> -o <dbdirectory>
上面命令会备份所有的数据库,如果只想备份某个数据库,则可以通过参数 -d <yourdbname>
来指定。备份完成后,会保存备份文件到 -o
指定的目录中,下面是备份目录中备份文件的一个例子:
$ find output output/ output/db1/tbl1.bson output/db1/tbl1.metadata.json output/db1/tbl2.bson output/db1/tbl2.metadata.json
恢复 mongodb:
$ mongorestore -h <hostname>:<port> -u <user> -p <password> <dbdirectory>
如果我们想把备份文件压缩为一个文件,则可以指定参数 --gzip --archive=file.gz
,如:
$ mongodump -h <hostname>:<port> -u <user> -p <password> --db <yourdbname> --gzip --archive=/path/to/archive.gz # 备份 $ mongorestore -h <hostname>:<port> -u <user> -p <password> --gzip --archive=/path/to/archive.gz # 恢复
3. MongoDB 集群操作
MongoDB 的集群搭建方式主要有三种:主从模式,Replica Set 模式(副本集模式),Sharding 模式。 其中,主从模式已经被副本集取代,目前副本集模式应用较为广泛,Sharding 模式提供了 MongoDB 的“水平扩展”能力,但配置维护较为复杂。
3.1. Replica Set
A replica set in MongoDB is a group of mongod processes that maintain the same data set. Replica sets provide redundancy and high availability.
一个副本集只能有一个主节点(Primary),可以有多个从节点(Secondaries),还可以有一个仲裁节点(当参与选举的节点无法选出主节点时仲裁节点充当仲裁的作用,仲裁节点不存储数据)。 主节点可以读写,而从节点不能写只能读。 主节点将数据修改操作记录在 operation log(简称 oplog)中,oplog 会自动复制到从节点中,从节点通过应用 oplog 到自己数据集来实现数据的同步。一般配置奇数个节点,至少三个节点,如图 1 所示。
Figure 1: MongoDB 的 Replica Set 模式
如果主节点宕机后会进行自动选举,选举出一个从节点为新的主节点,从而实现自动故障转移。
参考:
https://docs.mongodb.com/manual/replication/
https://docs.mongodb.com/manual/tutorial/deploy-replica-set/
3.1.1. 第一步、在每个机器上启动 MongoDB
后文将介绍如果部署 3 个节点的 MongoDB 副本集集群,假设 3 个节点的域名分别为表 3 所示。
Replica Set Member | Hostname |
---|---|
Member 0 | mongodb0.example.net |
Member 1 | mongodb1.example.net |
Member 2 | mongodb2.example.net |
在每个机器上启动 MongoDB 很简单,分别登录这 3 个机器,带 --replSet <name>
参数启动 mongod 即可:
$ mongod --replSet "rs0" --fork --config <path-to-config>
说明:这和启动普通的 mongod 节点没有什么区别,唯一的不同在于需要设置副本集的名字(这个例子中为 rs0)。注: 同一副本集群的 replSet 名称必需相同。 除了在命令行直接指定外,也可以在其配置文件中指定副本集的名字,如:
replication: replSetName: "rs0"
3.1.2. 第二步、在某一台机制上执行 rs.initiate 命令
登录 3 台机器中的任意一台(比如 mongodb0.example.net),进入到 mongo shell 中,执行 rs.initiate
命令,在这个命令的参数中指定副本集名字和其它节点的信息,比如:
rs.initiate( { _id : "rs0", members: [ { _id: 0, host: "mongodb0.example.net:27017" }, { _id: 1, host: "mongodb1.example.net:27017" }, { _id: 2, host: "mongodb2.example.net:27017" } ] })
3.1.3. 第三步、查看各个节点状态
通过前面的步骤,集群的基本配置已经完成了。在 mongo shell 中执行 rs.status()
可以查看各个节点状态。如:
...... "members" : [ { "_id" : 0, "name" : "mongodb0.example.net:27017", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 1125, ...... { "_id" : 1, "name" : "mongodb1.example.net:27018", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 735, ...... { "_id" : 2, "name" : "mongodb2.example.net:27019", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 735,
从输出中可知,mongodb0.example.net 是主节点,其它两个是从节点。
3.1.4. 集群的其它操作
MongoDB 还支持其它一些操作,如 rs.stepDown():将主节点降级为从节点;rs.add()增加节点;rs.remove()删除节点,等等。可以执行 rs.help()
查看帮助,如:
> rs.help() rs.status() { replSetGetStatus : 1 } checks repl set status rs.initiate() { replSetInitiate : null } initiates set with default settings rs.initiate(cfg) { replSetInitiate : cfg } initiates set with configuration cfg rs.conf() get the current configuration object from local.system.replset rs.reconfig(cfg) updates the configuration of a running replica set with cfg (disconnects) rs.add(hostportstr) add a new member to the set with default attributes (disconnects) rs.add(membercfgobj) add a new member to the set with extra attributes (disconnects) rs.addArb(hostportstr) add a new member which is arbiterOnly:true (disconnects) rs.stepDown([stepdownSecs, catchUpSecs]) step down as primary (disconnects) rs.syncFrom(hostportstr) make a secondary sync from the given member rs.freeze(secs) make a node ineligible to become primary for the time specified rs.remove(hostportstr) remove a host from the replica set (disconnects) rs.slaveOk() allow queries on secondary nodes rs.printReplicationInfo() check oplog size and time range rs.printSlaveReplicationInfo() check replica set members and replication lag db.isMaster() check who is primary
3.2. Sharding 集群
所谓 Sharding 就是将同一个集合的不同子集分发存储到不同的机器(shard)上,MongoDB 使用 Sharding 机制来支持超大数据量,将不同的 CRUD 路由到不同的机器上执行,以提到数据库的吞吐性能。
这里不重点介绍 Sharding 模式的配置,有兴趣的朋友可参考:https://docs.mongodb.com/manual/sharding/
4. MongoDB Change Streams(可监控数据变化)
Change Streams 是 MongoDB 3.6 中增加的功能。应用程序使用 Change Stream 可以订阅 Collection 的变化。Change Stream 是基于 oplog 实现的,所以这个功能只在 Replica Set 集群或 Sharding 集群中可用。
4.1. 实例程序
下面是 Change Stream 的实例程序(假设文件名为 test.js):
const MongoClient = require("mongodb").MongoClient; // npm i mongodb@3 // 假设MongoDB Replica Set集群的名字为rs0,集群服务器为localhost:27017,localhost:27018,localhost:27019 MongoClient.connect("mongodb://localhost:27017,localhost:27018,localhost:27019?replicaSet=rs0") .then(client => { console.log("Connected correctly to server"); // specify db and collections const db = client.db("testdb1"); const collection = db.collection("people"); const changeStream = collection.watch(); // start listen to changes changeStream.on("change", function(change) { console.log(change); // 数据库testdb1中集合people有变化时,这里都会输出 }); });
上面程序实现了监控数据库 testdb1 中集合 people,一旦有变化就会输出变化内容。
执行上面程序,会输出下面内容后一直等待。
$ node test.js Connected correctly to server
打开一个 mongo shell,我们往数据库 testdb1 中集合 people 中插入一条数据,如下:
rs0:PRIMARY> use testdb1 switched to db testdb1 rs0:PRIMARY> db.people.insert({"name" : "cig01"}) WriteResult({ "nInserted" : 1 })
这时,运行前面程序的窗口会显示:
Connected correctly to server { _id: { _data: Binary { _bsontype: 'Binary', sub_type: 0, position: 49, buffer: <Buffer 82 5b e6 98 87 00 00 00 02 46 64 5f 69 64 00 64 5b e6 98 87 3b 60 bc 76 02 ae 0f 37 00 5a 10 04 6a 58 86 6b f4 11 42 b4 8a f6 3a ec 8e 52 86 7b 04> } }, operationType: 'insert', fullDocument: { _id: 5be698873b60bc7602ae0f37, name: 'cig01' }, ns: { db: 'testdb1', coll: 'people' }, documentKey: { _id: 5be698873b60bc7602ae0f37 } }