From 5398fedd9d100cc089408ac178abcb7fd93635ea Mon Sep 17 00:00:00 2001 From: jaysunxiao Date: Thu, 24 Jun 2021 13:09:10 +0800 Subject: [PATCH] =?UTF-8?q?doc[orm]:=20=E5=A2=9E=E5=8A=A0mongodb=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orm/mogodb-setup.md | 259 +++++++ orm/mongo-replica.md | 222 ++++++ orm/mongo-shard.md | 426 +++++++++++ orm/mongodb-transaction.md | 66 ++ orm/mongodb.md | 688 ++++++++++++++++++ .../com/zfoo/orm/client/SingleMongoTest.java | 374 ++++++++++ .../java/com/zfoo/orm/client/Student.java | 38 + 7 files changed, 2073 insertions(+) create mode 100644 orm/mogodb-setup.md create mode 100644 orm/mongo-replica.md create mode 100644 orm/mongo-shard.md create mode 100644 orm/mongodb-transaction.md create mode 100644 orm/mongodb.md create mode 100644 orm/src/test/java/com/zfoo/orm/client/SingleMongoTest.java create mode 100644 orm/src/test/java/com/zfoo/orm/client/Student.java diff --git a/orm/mogodb-setup.md b/orm/mogodb-setup.md new file mode 100644 index 00000000..6236d227 --- /dev/null +++ b/orm/mogodb-setup.md @@ -0,0 +1,259 @@ +# 一、MongoDB简介 + +## 1.MongoDB的特点 + +1. 数据文件为二进制Bson,一种Json的扩展 +2. 模式自由,在生产环境可以轻松增加字段 +3. 丰富的查询 +4. 完整的索引支持 +5. 复制和故障恢复 +6. 自动分片 +7. 客户端支持多种语言 +8. 支持内存映射存储引擎 + +## 2.MongoDB的适用场景 + +- MongoDB 4.2 以后支持分布式事务,理论上适合所有场景 + +# 二、MongoDB安装 + +## Windows + +### 1. MongoDB安装配置 + +1. 直接安装MongoDB,用custom自定义安装目录,一般用默认的安装目录即可,不要勾选mongoDB compass +2. 将C:\Program Files\MongoDB\Server\4.2\bin配置到环境变量中,便于全局使用 +3. mongo -version # 查看安装的版本 +4. MongoDB的工具 + +``` +bsondump # bson转换工具 +mongodump # 逻辑备份工具 +mongorestore # 逻辑恢复工具 +mongoexport # 数据导出工具 +mongoimport # 数据导入工具 +mongofiles # GriedFS文件工具 +mongos # 分片路由工具 +mongostat # 状态监控工具 +mongotop # 读写监控工具 +``` + +### 2. studio 3t安装(MongoDB可视化工具),收费 + +- 可视化工具直接默认安装即可 +- 3t的补丁 + +``` +每次开机重启脚本重置试用时间,即可重新获得使用权Studio 3T,重置studio 3t的试用时间解决无法使用的问题,并非永久破解。 +1. 将批处理文件studio3t.bat剪贴或复制到如下路径: + C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp +2. 双击重置时间 +``` + +### 3. MongoDB Compass + +- 官方工具,免费,推荐使用 + +## Linux + +### 1.MongoDB安装配置 + +``` +把MongoDB下载到/usr/local目录下,在/usr/local下新建文件夹MongoDB +``` + +- tar -zxvf mongodb-linux-x86_64-enterprise-rhel70-4.2.0.tgz -C /usr/local +- rename /usr/local/mongodb-linux-x86_64-enterprise-rhel70-4.2.0 mongodb + /usr/local/mongodb-linux-x86_64-enterprise-rhel70-4.2.0 +- vim /usr/local/mongodb/mongodb.config # 创建自定义配置文件,解压的mongodb安装包没有默认的配置文件 + +- wiredTiger + +```yaml +#security: +# authorization: enabled +systemLog: + destination: file + path: "/data/mongodb/logs/mongodb.log" + logAppend: true +storage: + engine: wiredTiger + dbPath: "/data/mongodb/db" + directoryPerDB: true + +# MongoDB默认端口是27017,为了安全,可以修改这个端口,避免恶意的连接尝试 +# 生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +net: + bindIp: 0.0.0.0 + port: 22400 + maxIncomingConnections: 900 +processManagement: + fork: true +``` + +- inMemory + +```yaml +#security: +# authorization: enabled +systemLog: + destination: file + path: "/data/mongodb/logs/mongodb.log" + logAppend: true +storage: + engine: inMemory + dbPath: "/data/mongodb/db" + +# MongoDB默认端口是27017,为了安全,可以修改这个端口,避免恶意的连接尝试 +# 生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +net: + bindIp: 0.0.0.0 + port: 22400 +processManagement: + fork: true +``` + +- mkdir -p /data/mongodb/db /data/mongodb/logs +- touch /data/mongodb/logs/mongodb.log + +- vim /etc/profile # 添加命令运行环境变量,在文件最后面添加下面两 + +``` +JAVA_HOME=/usr/local/java +JRE_HOME=$JAVA_HOME/lib + +MONGODB_HOME=/usr/local/mongodb + +PATH=$JAVA_HOME/bin:$MONGODB_HOME/bin:$PATH + +export JAVA_HOME JRE_HOME MONGODB_HOME PATH +``` + +- source /etc/profile,加载环境变量 + +- mongod --config /usr/local/mongodb/mongodb.config # 启动MongoDB + +``` +mongod: error while loading shared libraries: libnetsnmpmibs.so.31: cannot open shared object file: No such file or directory +如果启动看到上面这个错误,则是缺少net-snmp安装包,yum install net-snmp +``` + +- mongo -port 22400 # 使用mongo客户端链接MongoDB + +### 2.关闭MongoDB服务 + +- 方式一:进入mongo客户端关闭(推荐方式) + +``` +mongo -port 22400 +use admin +db.shutdownServer() +``` + +- 方式二:kill -2 pid 或者 kill -15 pid + +``` +-2 和 -15 都会等MongoDB处理完,释放相应资源再停止。kill -9 会强制服务i停止,会导致数据的顺坏,不建议使用。 +``` + +- 方式三:如果MongoDB注册为service,可以使用服务的命令关闭 + +``` +systemctl stop mongodb +``` + +- 修复未正常关闭MongoDB,导致无法启动 + +``` +进入/data/mongodb/db,将文件夹下的mongod.lock(也可以使用find命令查找)删除。 +如果还有数据顺坏,需要使用mongod --repair命令修复一次,再正常启动。 +修复数据库的实际过程很简单:将所有的文档导出后马上导入,忽略无效的文档,完成后会重建索引。 +``` + +### 3.将MongoDB设置为开机自动启动 + +- vim /usr/lib/systemd/system/mongodb.service,创建启动脚本,systemctl是最新的启动命令,避免用service +- chmod 754 /usr/lib/systemd/system/mongodb.service,赋予启动脚本可执行的权限 + +```bash +[Unit] +#服务描述 +Description=High-performance, schema-free document-oriented database +#指定了在systemd在执行完那些target之后再启动该服务 +After=network.target + +[Service] +#定义Service的运行类型,一般是forking(后台运行) +Type=forking + +ExecStart=/usr/local/mongodb/bin/mongod --config /usr/local/mongodb/mongodb.config +ExecReload= +ExecStop=/usr/bin/pkill mongod + +[Install] +#多用户 +WantedBy=multi-user.target +``` + +- systemctl daemon-reload,重新加载服务 +- systemctl enable mongodb,会有一行反馈 +- systemctl status mongodb,注意看 -> enabled; vendor preset: disabled) +- systemctl start mongodb + +### 4.安全和访问控制 + +#### windows不用设置用户,为了方便便于开发 + +#### Linux用户设置 + +- 添加用户之前,要在数据库admin里添加一个拥有userAdminAnyDatabase角色的管理员账号,这个账号可以管理数据库admin和创建其它用户 +- 创建管理员帐号,账号信息保存在admin数据库 + +``` +use admin +db.createUser({ user: "root", pwd: "zfoo*5qtWy?cwajCGQl.Jh7o8D%X24A;", roles: [ + { role: "userAdminAnyDatabase", db: "admin" }, + { role: "clusterAdmin", db: "admin" }, + { role: "readWriteAnyDatabase", db: "admin" } + ]}) +``` + +- 创建普通帐号,账号信息保存在test数据库 + +``` +use test +db.createUser({ user: "test", pwd: "123456", roles: [ { role: "readWrite", db: "test" }, { role: "read", db: "reporting" }]}) +``` + +- 查看用户权限 + +``` +use test +db.getUser("myTester") +db.getRole( "read", { showPrivileges:true } ) # 查看权限能执行哪些操作,例如要看test数据库中read权限能执行哪些操作 +db.grantRolesToUser( "test", [{ role: "readWrite", db: "reporting" }]) # 授权 +db.revokeRolesFromUser( "test", [{ role: "readWrite", db: "reporting" }]) # 取消权限 +``` + +- 添加完成后,在配置文件中取消注释,#authorization +- 再重启mongod服务:systemctl restart mongodb + +- 用户登录 + +``` +# 启动mongo客户端时登录,--authenticationDatabase "admin"表示myUserAdmin用户在admin数据库下 +# 远程用户登录注意打开防火墙的端口,或者直接关闭防火墙 +mongo --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" + + +# 进入mongo客户端后再登录: +mongo --port 22400 +use admin +db.auth("root", "123456" ) + +# 修改密码,将root的密码修改为456789,需要admin管理员权限。 +db.changeUserPassword("root", "456789") + +# 删除用户,需要admin管理员权限 +db.dropUser("root") +``` diff --git a/orm/mongo-replica.md b/orm/mongo-replica.md new file mode 100644 index 00000000..eca8dd09 --- /dev/null +++ b/orm/mongo-replica.md @@ -0,0 +1,222 @@ +# 一、副本集的搭建 + +## 1.MongoDB环境准备 + +- 三台虚拟机的静态ip分别设置为 + +``` +192.168.1.30 mongodb0 +192.168.1.31 mongodb1 +192.168.1.32 mongodb2 +``` + +- 设置好ip后还需要把ip与主机名对应起来 + +``` +hostname # 查看主机名使用命令 + + +vim /etc/sysconfig/network,修改主机名,在最后一行加上HOSTNAME=mongodb0,不同主机的对于关系如下 +192.168.1.30 -> HOSTNAME=mongodb0 +192.168.1.31 -> HOSTNAME=mongodb1 +192.168.1.32 -> HOSTNAME=mongodb2 + +vim /etc/hosts,修改为我们设置的主机名,不同的主机对应不同的mongodb +127.0.0.1 localhost mongodb0 localhost4 localhost4.localdomain4 +::1 localhost mongodb0 localhost6 localhost6.localdomain6 +192.168.1.30 mongodb0 +192.168.1.31 mongodb1 +192.168.1.32 mongodb2 + +``` + +- 重启服务器后生效,使用hostname命令验证主机名,使用ping命令验证三个虚拟机之间相互可以使用主机名ping通。 +- 开启防火墙端口。 + +## 2.创建目录和修改配置文件 + +- mongodb0,mongodb1,mongodb2使用命令 + +``` +mkdir -p /data/mongodb/db # 存放数据的目录 +mkdir -p /data/mongodb/logs # 存放日记的目录 +mkdir -p /data/mongodb/key # 存放密钥的目录 +``` + +- vim /usr/local/mongodb/mongodb.config,修改配置文件如下 + +``` +dbpath=/data/mongodb/db + +logpath=/data/mongodb/logs/mongodb.log +logappend=true + +port=22400 +fork=true + +#keyFile默认会开启auth=true +#auth=true +#keyFile=/data/mongodb/key/myKey.txt +replSet=myReplicaSet + +#生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +bind_ip=0.0.0.0 +``` + +## 3.初始化副本集 + +- 清空一下目录的历史数据,因为可能以前是用的单机节点,集群部署会有不可预知问题,所以先清空数据 + +``` +/data/mongodb/db +/data/mongodb/logs +``` + +- 分别启动mongodb0,mongodb1,mongodb2 + +``` +systemctl restart mongodb +``` + +- 创建副本集 + +``` +mongo --port 22400 # 进入mongodb0 +config = {_id: "myReplicaSet", members: [{_id: 0, host:"mongodb0:22400"}, {_id: 1, host:"mongodb1:22400"}, {_id: 2, host:"mongodb2:22400", arbiterOnly : true}]} +rs.initiate(config) # 初始化副本集 +``` + +- 观察副本集 + +``` +rs.conf() # 副本集配置 +rs.status() # 副本集状态 +``` + +## 4.副本集初始化后,需要给整个副本集创建帐户、密码 + +- 创建Key,mongodb0,mongodb1,mongodb2使用命令 + +``` +echo "replicaSet key" > /data/mongodb/key/myKey.txt +chmod 600 /data/mongodb/key/myKey.txt +``` + +- 在主节点上,用客户端连接,创建用户权限(主节点,可以用 rs.status() 查看),创建用户请看mongodb-setup.md + +- 关闭副本集,分别关闭mongodb0,mongodb1,mongodb2 + +``` +systemctl stop mongodb +``` + +- vim /usr/local/mongodb/mongodb.config,修改配置文件如下 + +``` +dbpath=/data/mongodb/db + +logpath=/data/mongodb/logs/mongodb.log +logappend=true + +port=22400 +fork=true + +#keyFile默认会开启auth=true +auth=true +keyFile=/data/mongodb/key/myKey.txt +replSet=myReplicaSet + +#生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +bind_ip=0.0.0.0 +``` + +- 重新启动副本集,分别启动mongodb0,mongodb1,mongodb2 + +``` +systemctl restart mongodb +``` + +## 5.数据同步测试 + +- 首先向PRIMARY(主节点)写入一条数据 + +``` +use test +db.say.insert({"text":"Hello World"}) +db.say.find() +``` + +- 进入SECONDARY(副节点)查看数据是否同步 + +``` +默认情况下SECONDARY节点不能读写,要设定slaveOk为true才可以从SECONDARY节点读取数据。 +replSet里只能有一个Primary节点,只能在Primary写数据,不能在SECONDARY写数据。 +首先需要在SECONDARY节点设置slaveOk为true + +db.getMongo().setSlaveOk(); +use test; +db.say.find(); +``` + +## 6.故障切换测试 + +- 把主节点关点,看看副集点是否能接替主节点进行工作 + +# 二、原理解析 + +## 1.副本集实现了数据库的数据同步,故障转移,读写分离 + +副本集最少应该包含三个节点,其中有一个必须是主节点。 + +数据同步:从节点会从主节点同步oplog到自己的节点,oplog有自己的大小,可以通过--oplogSize来设置。主从之间的读写对程序员是透明的。 +故障转移(容灾):主从节点两秒钟就会有一个心跳,如果发现主节点挂了就从新选取主节点,选取规则是从节点中数据最新的,权重最高的节点。 如果从节点挂了就直接选取从节点,再将主节点的数据同步给从节点。 +读写分离:可以设置是优先从主节点读数据,还是优先从从节点读数据。 注:在单个节点中,数据先被写到内存,操作被保存在内存的视图中,内存视图没100ms写到硬盘的journal日志中,mongodb每60s将内存中的数据同步到硬盘。 + +# 三、副本集的管理 + +## 1.诊断 + +- 副本集中的机子,查看主数据库的复制的状态 + +``` +db.printReplicationInfo() + +configured oplog size: 990MB +log length start to end: 174676secs (48.52hrs) +oplog first event time: Wed May 01 2019 13:29:38 GMT+0800 +oplog last event time: Fri May 03 2019 14:00:54 GMT+0800 +now: Fri May 03 2019 14:00:56 GMT+0800 + +输出信息oplog大小990MB +log length start to end = oplog last event time - oplog first event time +因为oplog是固定大小,有一个循环,所以必须等oplog循环几圈,oplog才和实际相符合。 + +如果oplog太小,写入的数据太快,oplog中的日志可能会被顶掉,从节点想要同步时候发现找不到切入的oplog,已经跟不上主节点了,就会停止同步。 +如果log length start to end和下面的从节点behind the primary非常的接近,说明oplog可能太小了 +``` + +- 查看从数据库的复制的状态 + +``` +db.printSlaveReplicationInfo() + +source: mongodb-config1:22400 + syncedTo: Fri May 03 2019 13:56:31 GMT+0800 + 0 secs (0 hrs) behind the primary +source: mongodb-config2:22400 + syncedTo: Fri May 03 2019 13:56:31 GMT+0800 + 0 secs (0 hrs) behind the primary + +``` + +## 2.副本集回滚丢失的数据 + +``` +主节点故障后重新加入副本集时会回滚恢复写操作。 +只有在主节点接受了写请求并且从节点没有复制前主节点就故障的时候才需要回滚。 +当主节点作为从节点重新加入副本集,他恢复或回滚他的写操作来与其他成员保持数据库一致性。 + +当回滚确实发生了,MongoDB把回滚数据写入数据库的dbPath的rollback文件夹下的BSON文件中,可以用bsondump读取回滚文件的内容。 +回滚文件名如下形式: +...bson +``` diff --git a/orm/mongo-shard.md b/orm/mongo-shard.md new file mode 100644 index 00000000..58c427e8 --- /dev/null +++ b/orm/mongo-shard.md @@ -0,0 +1,426 @@ +# 一、分片的搭建 + +## 1.MongoDB环境准备 + +- config服务器三台,虚拟机的静态ip分别设置为 + +``` +192.168.1.40 mongodb-config0 +192.168.1.41 mongodb-config1 +192.168.1.42 mongodb-config2 +``` + +- route服务器三台,虚拟机的静态ip分别设置为 + +``` +192.168.1.45 mongodb-route0 +192.168.1.46 mongodb-route1 +192.168.1.47 mongodb-route2 +``` + +- shard服务器三台,虚拟机的静态ip分别设置为 + +``` +192.168.1.50 mongodb-shard0 +192.168.1.51 mongodb-shard1 +192.168.1.52 mongodb-shard2 +``` + +- shard副本集服务器三台,虚拟机的静态ip分别设置为 + +``` +192.168.1.55 mongodb-shard3-replica0 +192.168.1.56 mongodb-shard3-replica1 +192.168.1.57 mongodb-shard3-replica2 +``` + +- 设置好ip后还需要把ip与主机名对应起来 + +``` +hostname # 查看主机名使用命令 + +vim /etc/sysconfig/network,修改主机名,在最后一行加上HOSTNAME=mongodb-config0,其它的服务器同理 + +vim /etc/hosts,修改为我们设置的主机名,不同的主机对应不同的mongodb +127.0.0.1 localhost mongodb-config0 localhost4 localhost4.localdomain4 +::1 localhost mongodb-config0 localhost6 localhost6.localdomain6 + +192.168.1.30 mongodb0 +192.168.1.31 mongodb1 +192.168.1.32 mongodb2 + +192.168.1.40 mongodb-config0 +192.168.1.41 mongodb-config1 +192.168.1.42 mongodb-config2 + +192.168.1.45 mongodb-route0 +192.168.1.46 mongodb-route1 +192.168.1.47 mongodb-route2 + +192.168.1.50 mongodb-shard0 +192.168.1.51 mongodb-shard1 +192.168.1.52 mongodb-shard2 +192.168.1.55 mongodb-shard3-replica0 +192.168.1.56 mongodb-shard3-replica1 +192.168.1.57 mongodb-shard3-replica2 +``` + +- 重启服务器后生效,使用hostname命令验证主机名,使用ping命令验证三个虚拟机之间相互可以使用主机名ping通。 +- 开启防火墙端口。 + +## 2.config服务器 + +- mongodb-config0,mongodb-config1,mongodb-config2使用命令 + +``` +mkdir -p /data/mongodb/db # 存放数据的目录 +mkdir -p /data/mongodb/logs # 存放日记的目录 +mkdir -p /data/mongodb/key # 存放密钥的目录 +``` + +- vim /usr/local/mongodb/mongodb.config,修改配置文件如下 + +``` +dbpath=/data/mongodb/db + +logpath=/data/mongodb/logs/mongodb.log +logappend=true + +port=22400 +fork=true + +configsvr=true +replSet=mongodbConfigSet + +#keyFile默认会开启auth=true +#auth=true +#keyFile=/data/mongodb/key/myKey.txt + +#生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +bind_ip=0.0.0.0 +``` + +- 创建副本集 + +``` +mongo --port 22400 # 进入mongodb0 +config = {_id: "mongodbConfigSet", members: [ + {_id: 0, host:"mongodb-config0:22400"}, + {_id: 1, host:"mongodb-config1:22400"}, + {_id: 2, host:"mongodb-config2:22400"} + ]} +rs.initiate(config) # 初始化副本集 +``` + +- 观察副本集 + +``` +rs.conf() # 副本集配置 +rs.status() # 副本集状态 +``` + +## 3.route路由服务器 + +- mongodb-route0,mongodb-route1,mongodb-route2使用命令 + +``` +mkdir -p /data/mongodb/db # 存放数据的目录 +mkdir -p /data/mongodb/logs # 存放日记的目录 +mkdir -p /data/mongodb/key # 存放密钥的目录 +``` + +- vim /usr/local/mongodb/mongodb.config,修改配置文件如下 + +``` +logpath=/data/mongodb/logs/mongodb.log +logappend=true + +port=22400 +fork=true + +#必须是1个或则3 +configdb=mongodbConfigSet/mongodb-config0:22400,mongodb-config1:22400,mongodb-config2:22400 + +#keyFile默认会开启auth=true +#keyFile=/data/mongodb/key/myKey.txt + +#生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +bind_ip=0.0.0.0 +``` + +- vim /usr/lib/systemd/system/mongodb.service,修改启动脚本 + +```bash +[Unit] +#服务描述 +Description=High-performance, schema-free document-oriented database +#指定了在systemd在执行完那些target之后再启动该服务 +After=network.target + +[Service] +#定义Service的运行类型,一般是forking(后台运行) +Type=forking + +ExecStart=/usr/local/mongodb/bin/mongos --config /usr/local/mongodb/mongodb.config +ExecReload= +ExecStop=/usr/bin/pkill mongos + +[Install] +#多用户 +WantedBy=multi-user.target +``` + +- systemctl daemon-reload,重新加载服务 +- systemctl restart mongodb + +## 4.shard分片服务器 + +- mongodb-shard0,mongodb-shard1,mongodb-shard2使用命令 + +``` +mkdir -p /data/mongodb/db # 存放数据的目录 +mkdir -p /data/mongodb/logs # 存放日记的目录 +mkdir -p /data/mongodb/key # 存放密钥的目录 +``` + +- vim /usr/local/mongodb/mongodb.config,修改配置文件如下 + +``` +dbpath=/data/mongodb/db + +logpath=/data/mongodb/logs/mongodb.log +logappend=true + +port=22400 +fork=true + +shardsvr=true + +#keyFile默认会开启auth=true +#auth=true +#keyFile=/data/mongodb/key/myKey.txt + +#生产环境需要设置ip:bind_ip=127.0.0.1,本机ip +bind_ip=0.0.0.0 +``` + +- systemctl restart mongodb + +## 5.配置sharding + +- mongo客户端进入mongos服务所在的端口,在其中一台机子中进入mongos + +``` +mongo --port 22400 +use admin # 需要先切换到admin数据库,才能进行分片的添加 +sh.addShard("mongodb-shard0:22400") +sh.addShard("mongodb-shard1:22400") +sh.addShard("mongodb-shard2:22400") + +# 单节点分片的缺点是如果其中一个分片挂了,会导致整个分片集群不可用,所以建议每个分片都使用副本集 +sh.addShard("mongodbShard3Set/mongodb-shard3-replica0:22400,mongodb-shard3-replica1:22400,mongodb-shard3-replica2:22400") +``` + +## 6.使用分片数据库和使用分片集合 + +- 在任节点中进入mongos + +``` +mongo --port 22400 # 切换到admin数据库 +use admin +sh.enableSharding("test") + +sh.shardCollection("test.student", { _id: "hashed" }) # 把test数据库student集合的_id字段设置为哈希片键 +sh.shardCollection("test.student", { _id:1 }) # 把test数据库student集合的_id字段设置为区间片键 +``` + +- 标签片键 + +``` +标签片键比较特别,通过对分片节点打标签,再将片键按范围对应到这些标签上,对应片键范围的集合中的数据就会落在这个分片节点上。 +首先需要对分片打标签,对分片打标签需要知道分片的_id,在mongos中使用命令查看: +sh.status() + + +这里分片_id分别为: shard0000, shard0001, shard0002,则对分片打标签命令如下: +sh.addShardTag("shard0000", "tag1") +sh.addShardTag("shard0001", "tag2") +sh.addShardTag("shard0002", "tag3") + + +然后student的学号字段code设置标签片键范围。 +这样设置后学号code为1到600的会存储在分片shard0001或者shard0002中,学号为601到5000的会存储在分片shard0000中。 +sh.addTagRange("test.student", { _id: 0 }, { _id: 1000 }, "tag1") +sh.addTagRange("test.student", { _id: 1001 }, { _id: 9000 }, "tag2") +sh.addTagRange("test.student", { _id: 9001 }, { _id: 10000 }, "tag3") + +查看标签的分片,在mongos中使用命令 +use config +db.shards.find() + +查看标签的片键值范围,使用命令 +use config +db.tags.find() + + +移除某个分片的标签使用命令 +sh.removeShardTag("shard0000", "tag1") +sh.removeShardTag("shard0001", "tag2") +sh.removeShardTag("shard0002", "tag3") + +``` + +## 7.分片数据测试 + +- 集合测试 + +``` +mongo --port 22400 +use test +for(var i=0; i<10000; i++) db.student.insert({ _id:i, name:"jaysunxiao", addr:"myAddress", country:"myCountry" }) +执行可能需要一段会时间,我们直接在工具中查看config集合中的chunks元数据信息,就可以看到随着数据的写入分了多少片,以及片键的最小到最大的范围。 + +3个shard服务器里可以看到都有了mytest数据库以及student集合。 +``` + +## 8.分片的管理 + +- 移除Shard Server,回收数据,分片被删除之后,数据被移到其他分片中,不会丢失 + +``` +use admin +# 如果是区间片键,已经指定了区间了,数据迁移会失败,会导致分片一直draining,因为找不到区间分片的节点 +db.runCommand( { removeShard: "shard0002" } ) +# 检查迁移的状态,再次在 admin 数据库运行 removeShard 命令,因为数据多的话要一处的节点需要很长时间迁移数据 +db.runCommand( { removeShard: "shard0002" } ) + +db.runCommand({"moveprimary": "test", "to": "shard0000"}) # 要是原来分片是一个主分片,则需要执行movePrimary +``` + +## 9.均衡器 + +- 均衡器:均衡器负责数据迁移,周期性的检查分片是否存在不均衡。 config.locks集合里的state表示均衡器是否找正在运行,0表示非活动状态,2表示正在均衡。 + 均衡迁移数据的过程会增加系统的负载:目标分片必须查询源分片的所有文档,将文档插入目标分片中,再清除源分片的数据。 可以关闭均衡器(不建议):关闭会导致各分片数据分布不均衡,磁盘空间得不到有效的利用。 + +``` +sh.setBalancerState(false) #关闭自动均衡器,手动均衡 +sh.setBalancerState(true) + +use config +db.settings.find() #查看均衡器状态 + +# 移动块的时候必须关闭均衡器,否则可能导致死循环 +use admin +sh.moveChunk("db.collection",{块地址},"新片名称") #手动均衡 +sh.moveChunk("test.student",{"_id": "test.student-_id_6148914691236517204"},"shard0000") +use config +db.chunks.find().pretty() +``` + +## 10.认证分配 + +- 首先创建账号(Root角色),其中route(mongos)不需要创建账号,需要两种权限 + +``` +userAdminAnyDatabase # 管理员权限,用来管理其它的账号 +clusterAdmin # 分片管理员权限 +``` + +- 再生成一个KeyFile文件 +- config,shard节点修改配置,vim /usr/local/mongodb/mongodb.config + +``` +#keyFile默认会开启auth=true +auth=true +keyFile=/data/mongodb/key/myKey.txt +``` + +- route节点修改配置,vim /usr/local/mongodb/mongodb.config + +``` +#因为mongos本来就是从config里加载数据的,所以只需要添加keyfile文件即可 +keyFile=/data/mongodb/key/myKey.txt +``` + +- 重新启动各个节点 + +``` +systemctl restart mongodb +``` + +## 11.分片备份、还原 + +- 先停止平衡器的工作,并检查没有chunk move动作,保证dump的时候没有进行数据迁移。 + +``` +sh.stopBalancer() +``` + +- 锁定数据库,保证数据没有写入:在各个分片上和配置服务器上执行。 + +``` +db.fsyncLock() +``` + +- 执行备份操作mongodump,备份各个分片服务器和配置服务器。 + +- 解锁数据库,备份完成之后在分片和配置服务器上解锁数据库,允许修改。 + +``` +db.fsyncUnlock() +``` + +- 备份还原mongorestore各个分片和配置服务器 +- 重启各个实例 + +# 二、原理解析 + +## 1.分片实现了数据的分布式存储,负载均衡,这些都是海量数据的云存储平台不可或缺的功能 + +- 配置服务器 + +``` +是一个独立的mongod进程,保存集群和分片的元数据,即各分片包含了哪些数据的信息。 +最先开始建立,启用日志功能。像启动普通的mongod一样启动配置服务器,指定configsvr选项。 +不需要太多的空间和资源,配置服务器的1KB空间相当于真实数据的200MB。保存的只是数据的分布表。 +当服务不可用,则变成只读,无法分块、迁移数据。 +``` + +- 路由服务器 + +``` +即mongos,起到一个路由的功能,供程序连接。 +本身不保存数据,在启动时从配置服务器加载集群信息,开启mongos进程需要知道配置服务器的地址,指定configdb选项。 +``` + +- 分片服务器 + +``` +是一个独立普通的mongod进程,保存数据信息。可以是一个副本集也可以是单独的一台服务器。 +``` + +## 2.mongodb什么时候开始分片? + +MongoDB的分片是基于范围的,也就是说任何一个文档一定位于指定片键的某个范围。一旦片键选择好后,chunks就会按照片键来将一部分documents从逻辑上组合在一起。 +由于chunks的大小默认的阈值是64 MB或100000个文档,一旦达到这个阈值就会根据片键分片。 +片键一般是是索引,如果没有指定索引,则用集合默认的_id索引作为片键。 + +## 3.分片适用的场景 + +1,机器的磁盘不够用了。使用分片解决磁盘空间的问题。 2,单个mongod已经不能满足写数据的性能要求。通过分片让写压力分散到各个分片上面,使用分片服务器自身的资源。 +3,想把大量数据放到内存里提高性能。和上面一样,通过分片使用分片服务器自身的资源。 + +## 4.集群dump备份恢复策略,对集群的备份就可以转化为对各复制集的备份: + +1. 禁用平衡器,命令:sh.stopBalancer();因为分片集群上会有一个balancer进程在后台维护各个片上数据块数量的均衡,如果不禁用平衡器可能会导致备份数据的重复或缺失 +2. 停止毎个片(副本集)上的某个secondary节点,利用此节点进行备份;停止其中某个配置服务器(所有配置服务器的数据一样),保证备份时配务器上元数据不会改变,备份可以当作一单节点的实例 +3. 重启所有停掉的复制集成员,它们会自动从primary节点上的oplog同步数据,最终数据会达到一致性 +4. 重启分片集群的平衡器。通过mongo连接到mongos上,执行命令如下。use config;sh.startBalancer() + +## 5.集群恢复流程 + +1. 停止集群上的所有mongod实例和mongos实例。 +2. 利用上面备份的dump文件,依次恢复片中的每个副本集 +3. 恢复配置服务器。 +4. 重启所有mongod实例与mongos实例。 +5. 通过mongo连接上mongos,执行以下命令确保集群是可操作的:db.printShardingStatus() diff --git a/orm/mongodb-transaction.md b/orm/mongodb-transaction.md new file mode 100644 index 00000000..d35f258c --- /dev/null +++ b/orm/mongodb-transaction.md @@ -0,0 +1,66 @@ +# 一、安全写入数据 + +## 1.MongoDB的四种写入级别 + +- Unacknowledged 非确认式写入 + +``` +{ writeConcern:{w:0}} +非确认式写入不返回响应结果。 +``` + +- Acknowledged 确认式写入,mogodb客户端默认的写入模式 + +``` +确认式写入返回写入失败的错误信息,比如DuplicateKey Error。使用命令: +{ writeConcern:{w:1}} +``` + +- Journaled 日志写入 + +``` +一般的写入完成只是写入到内存中,并没有持久化到硬盘,日志写入模式会写入完成之后把记录保存到journal日志后才返回响应结果,这种写入方式能够承受服务器突然断电崩溃,更有效的保障数据的安全。 +{ writeConcern:{j:true}} +``` + +- Replica Acknowledged 复制集确认式写入 + +``` +写操作不仅要得到主节点的写入确认,还需要得到从节点的写入确认,这里还可以设置写入节点的个数。这种方式适用于对写入安全要求更高的场景。 +{ writeConcern:{w:2}} +``` + +# 二、Journal日志 + +## 1.单机开启Journal,多机器使用副本集 + +``` +Journal是MongoDB中的redolog(MySQL),而Oplog则负责副本集的同步(binlog MySQL) +不开启Journal,数据会写入内存,一般每60秒写入一次到硬盘 +开启Journal,每100毫秒左右往Journal日志文件写入一次数据(顺序写速度快),即使宕机,结果Journal恢复也只丢死100毫秒数据 +``` + +## 2.生产环境不要信任repair恢复的数据 + +``` +如果数据库崩溃了,而且没有开启--journal的情况下,千万不要这些数据拿来就用,因为数据库能损坏,索引可能混乱。 + +如果开启了Journal就可以使用mongod --repair修复一次。修复过程是将所有的文档导出后马上导入,忽略无效的文档,完成后会重建索引。 +但是这不是最优的选择,因为可能会丢失数据而且需要大量的磁盘空间。repair是最差的做法,比较好的做法是用以前备份过的数据库恢复,或者使用副本集部署。 +``` + +# 三、锁 + +- 查看锁的情况 + +``` +db.serverStatus() +db.currentOp() +``` + +- 解锁 + +``` +db.currentOp() # 找出有锁状态的执行操作,找到opid,根据之前获取的opid +db.killOp(opid) # kill掉对应的操作 +``` diff --git a/orm/mongodb.md b/orm/mongodb.md new file mode 100644 index 00000000..1ec96125 --- /dev/null +++ b/orm/mongodb.md @@ -0,0 +1,688 @@ +# 一、MongoDB常用命令 + +## 1.数据库常用命令 + +``` +show dbs # 查询数据库列表 +use student # 该命令如果数据库不存在,将创建一个新的数据库,否则将返回现有的数据库。 +db.dropDatabase() # 删除新的数据库 +db.repairDatabase() # 整理数据库,不仅能整理碎片还可以回收磁盘空间,但是会产生锁,建议停服的时候做 +db.getName() # 查看当前使用的数据库 +db.version() # 当前db的版本 +db.getMongo() # 查看当前db的链接机器地址 +db.getPrevError() # 查询之前的错误信息 +db.resetError() # 清除错误记录 +db.help() # 查看数据库帮助文档 +``` + +## 2.集合常用命令 + +``` +show collections # 查看所有的集合 +db.createCollection("student") # 创建一个集合 +db.createCollection("log", {size: 20, capped:true, max: 100}); # 或者带参数创建固定集合 +db.student # 使用集合 +db.student.drop() # 删除当前集合 +db.student.validate() # 验证集合是否有问题,验证集合内容 + + +db.student.count() # 查询当前集合的数据条数 +db.student.dataSize() # 查看集合数据大小 +db.student.totalIndexSize() # 查看集合索引大小 +db.student.storageSize() # 集合分配的空间大小,包括未使用的空间 +db.student.totalSize() # 显示集合总大小,包括索引和数据的大小和分配空间的大小 +db.student.getDB() # 显示当前集合所在的db +db.student.stats() # 显示当前集合的状态 +db.student.getShardVersion() # 集合的分片版本信息 +db.student.renameCollection("students"); # 集合重命名 +db.printCollectionStats() # 显示当前db所有集合的状态信息 +db.student.help() # 查看集合命令帮助文档 +``` + +- 集合内存 + +``` +在MongoDB中并不需要创建集合。 当插入一些文档 MongoDB 会自动创建集合。 +capped: Boolean +(可选)如果为true,它启用上限集合。上限集合是一个固定大小的集合,当它达到其最大尺寸会自动覆盖最老的条目。 +如果指定true,则还需要指定参数的大小 +size: number (可选)指定的上限集合字节的最大尺寸。如果capped 是true,那么还需要指定这个字段 +autoIndexID Boolean (可选)如果为true,自动创建索引_id字段。默认的值是 false +max number (可选)指定上限集合允许的最大文件数 + +DB.createCollection("student", { capped : true, size : 6142800, autoIndexId : true, max : 10000 }) +DB.createCollection("student") +``` + +## 3. 文档常用命令 + +### 增 + +``` +db.student.insert({name:"jay", age: 100}) + +如果我们不指定_id参数插入的文档,那么 MongoDB 将为文档分配一个唯一的ObjectId。_id 是12个字节十六进制数在一个集合的每个文档是唯一的。 +0-3时间戳,4-6机器唯一标识的散列标识,7-8进程号PID,9-11计数器。前三个保证同一个机器不同进程产生的标识唯一,后一个保证同一个进程产生不同的标识。 +简介说就是在分布式系统中可以在本机产生全局唯一id。 +``` + +### 删 + +``` +db.student.remove({name : "jay"},true); # true是只删除一条,默认是false全部删除 +db.student.remove({name : "jay"}); +``` + +### 改 + +``` +db.student.save({name : "jay", age : 20}) # save如果数据库已经有这条数据,则会更新它;如果没有,则写入 + +db.student.update({name:"jay"},{$set:{age : 20, company: "google"}}); +db.student.update({name:"jay"},{$set:{age : 20}},{multi:true}); # $set修改器;multi默认false,只更新找到的第一条记录 +db.student.update({name:"jay"},{$unset:{company:1}}); # $unset用于取消字段,去掉文档中的某个字段 +db.student.update({name:"jay"},{$inc: {age: 50}}); # $inc用于增加或减少数值 +db.student.update({name:"jay"},{$push: {phone: {home:456789}}}); # $push用于把元素追加到数组中,如果字段不存在,会新增 +db.student.update({name:"jay"},{$pushAll: {phone: [{home:456789} # $pushAll用于把元素追加到数组中,多个元素 +db.student.update({name:"jay"},{$pull: {phone:{home: 456789} }}); # $pull删除数组中满足条件的元素 +db.student.update({name:"jay"},{$addToSet: {phone: {home:456789}}}); # $addToSet类似于push,但是只有这个值不在数组中才会增加 +db.student.update({name:"jay"},{$pop: {phone:1}}); # $pop删除数组中最后一个元素 +db.student.update({name:"jay"},{$rename: {phone:"call"}}); # $rename修改字段名称 +``` + +### 查 + +``` +db.student.find() +db.student.find().pretty(); # 如果需要结构化显示返回的文档可以加上pretty()方法 +db.student.find({name:"jay"},{age:1}) # 查询name伟jay,只显示age + +db.student.count({$or: [{age: 14}, {age: 28}]}); # 查询满足条件的文档数量 +``` + +## 4.索引 + +### 创建索引 + +``` +db.student.ensureIndex({age: 1}); # 1表示age索引按升序排序 +db.student.ensureIndex({name: 1, age: -1}); # -1表示同组文档按降序排序 + +例如我们有数据: +{"name":"jay",age:14} +{"name":"ad",age:14} +{"name":"ad",age:38} +{"name":"ad",age:24} +{"name":"ab",age:14} +使用db.student.ensureIndex({name: 1, age: -1})建议索引后,索引中的数据组织为: +{"name":"ab",age:14} +{"name":"ad",age:38} +{"name":"ad",age:24} +{"name":"ad",age:14} +{"name":"jay",age:14} +``` + +### 创建唯一索引 + +``` +db.student.ensureIndex({name:1},{unique:true}) # 如果创建唯一索引时已经存在重复项,通过dropDups在创建唯一索引时消除重复文档 +db.student.ensureIndex({name:1},{unique:true,dropDups:true}) +``` + +### 索引操作 + +``` +db.student.getIndexes(); # 查询集合所有索引 +db.student.totalIndexSize(); # 查看集合总索引记录大小 +db.student.reIndex(); # 修复索引 +db.student.dropIndex("name");# 删除指定索引 +``` + +### 索引调优explain + +``` +db.student.find({name: "jaysunxiao" }).explain(); +``` + +## 5.查询 + +### 单个文档大小 + +``` +Object.bsonsize(db.user.findOne({'account':'xxx'})) # 返回文档的bytes数量 +``` + +### 迭代器,一般用于遍历数据集。通过hasNext()判断是否有下一条数据,next()获取下一条数据 + +```javascript +var iterator= db.student.find(); +while(iterator.hasNext()){ + let temp=iterator.next(); + print(temp.name); +} + +// 迭代器还实现了迭代器接口,所以可以使用forEach。 +var iterator= db.student.find(); +iterator.forEach(function(temp){ + print(temp.name); +}); +``` + +### find语法 + +``` +find( query, fields) +query 查询条件,相当于sql的where +fields 查询的结果,字段映射,0不显示,1显示 +explain("allPlansExecution") 返回查询计划信息和查询计划的执行统计信息 + +db.student.find({}, { "_id":0, "name":1}).limit(9).skip(5).sort({"name":-1}).pretty() +``` + +### 条件查询 + +``` +db.student.find({name:"jay",age:16}) # 与操作 +db.student.find({$or: [{age: 14}, {age: 28}]}); # 或操作$or +db.student.find({age: {$gt: 20}}) # 大于$gt +db.student.find({age: {$lt: 20}}) # 小于$lt +db.student.find({age: {$gte: 20}}) # 大于等于$gte +db.student.find({age: {$lte: 20}}) # 小于等于$lte +db.student.find({name: { $type : "string" }}); # 类型查询$type + +db.student.find({age: {$exists: true}}) # 是否存在$exists +db.student.find({age: {$mod : [10, 0]}}); # 取模$mod +db.student.find({age: { "$ne" : 23}}) # 不等于$ne +db.student.find({name:{ "$in" : [ "jay" , "ab"]}}) # 包含$in +db.student.find({name:{$not:{"$in":["jay","ab"]}}})# 取非 +db.student.find({name:{ "$nin":["jay","ab"]}}) # 不包含$nin +``` + +### null查询 + +``` +db.student.find({company:null}) # 查询company字段为null的文档 +db.student.find({company:{$nin:[null]} }) # 查询company字段不为空的文档\ +``` + +### 正则查询 + +``` +db.student.find({"name":/^j/}) # 以j开头的字符串 +``` + +### 嵌套查询 + +``` +db.student.find({"phone":{"home" : 123321,"mobile" : 15820123123}}) # 指定完整的文档,查询出子文档完全匹配指定文档的文档。 +db.student.find({"phone.home":123321}) # 如果我们不知道子文档的完整文档,只知道子文档中一个字段的值,可以通过点查询。 +``` + +### 数组查询 + +``` +db.student.find({favorite_number :[6, 8]}); # 精确匹配查询 +db.student.find({favorite_number:6}); # 数组单元素查询 +db.student.find({favorite_number : {$all : [6, 8]}}); # 数组多元素查询 +db.student.find({favorite_number: {$size: 3}}); # $size数组长度查询 +db.student.find({},{favorite_number: {$slice: 2}}); # $slice返回数组子集 +db.student.find({},{favorite_number: {$slice: -2}}); +db.student.find({phone.home:123456}); # 点查询用于查询更复杂的数组,例如数组中包含的是子文档的情况 +db.student.find({phone.home:123456,"phone.mobile":13820123123}); # 数组的子文档如果有多个字段,查询出子文档同时满足两个条件的文档有两种方式 +db.student.find( { # 或者,两者等价 + phone: { + $elemMatch: { + home :123456, + mobile: 13820123123 +}}}) +``` + +### 索引查询 + +``` +db.student.find({favorite_number.0:6}); # 数组都有索引,例如[6,8],6是第0个元素,8是第1个元素(数组索引以0开头)。要查找某个元素指定值的文档可以使用点和索引值 +db.student.find({phone.2.home:123456}); # 点查询中只要数组的子文档里有一个home值满足查询值就会返回文档。如果我们要精确到第几个元素也可以用索引查询。 +``` + +### 高级查询$where + +- Javascript与$where结合使用,下面的全部等价 + +``` +db.student.find({age: {$gt: 18}}); +db.student.find({$where: "this.age > 18"}); +db.student.find("this.age > 18"); +f = function() {return this.age > 18}; db.student.find(f); +``` + +## 6.聚合查询 + +### count和distinct + +``` +db.student.count({age: {$gte: 18}}) # 数量查询count +db.runCommand({"distinct":"student", "key":"age"}) # 不同值distinct +``` + +### 分组group + +``` +db.collection.group({ + key:{field:1}, + initial:{count:0}, + cond:{}, + reduce: function ( curr, result ) { }, + keyf:function(doc){}, + finalize:function(result) {} +}) + + +数据如下: +{ "_id" : ObjectId("58d9b6afa159504ca6c572e0"), "name" : "jay", "age" : 28, "company" : "google", "name" : "a2" } +{ "_id" : ObjectId("58d9ba2d6097167df6313438"), "name" : "jay", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df6313439"), "name" : "ad", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df631343a"), "name" : "ad", "age" : 38 } +{ "_id" : ObjectId("58d9ba2d6097167df631343b"), "name" : "ad", "age" : 24 } +{ "_id" : ObjectId("58d9ba2e6097167df631343c"), "name" : "ab", "age" : 14 } + + +我们这里可以做一个统计:每个年龄有多少人,则使用age字段作为key,在reduce时进行同组的数量计数,finalize中组织结果文档格式。 +使用代码如下: +db.student.group({ + key:{age:1}, + initial:{count:0}, + cond:{"age":{$gt:13}}, + reduce: function(curr,result) { + result.count += 1; + }, + finalize:function(result) { + result.年龄=result.age; + result.人数=result.count; + } +}); + + +使用MapReduce做一个统计:每个年龄有多少人,this表示当前文档,使用this.age的值作为key。在reduce时进行同组的数量计数,finalize中组织结果文档格式。 +代码如下: +db.student.mapReduce( + function () { + emit(this.age, {age: this.age, count: 1}); + }, + function (key, values) { + var count = 0; + values.forEach(function(val) { + count += val.count; + }); + return {age: key, count: count}; + }, + { + out: { inline : 1 }, + finalize: function (key, reduced) { + return {"年龄": reduced.age, "人数": reduced.count}; + } + } +) +第一个function是map函数,提交两个参数key和value,数据会根据key的值进行分组,把同组的value的值放入values数组中。 +第二个function是reduce函数,key和values是第一个map函数的分组结果。 +第三个out指定结果集生成在什么地方,inline:1,表示结果集放在内存中,仅适用于结果集符合16MB限制的情况 +``` + +### $project + +``` +数据如下: +{ "_id" : ObjectId("58d9ba2d6097167df6313438"), "name" : { "first" : "adrian", "last" : "jay" }, "age" : 14, "phone" : 111128912345 } +使用代码: +db.student.aggregate( + [ + { $project: { _id:0, age: 1, phone: "123456789", lastName: "$name.last" } } + ] +) +$project映射结果集的文档结构,0表示排除字段,1显示字段,"123456789"直接赋值 +``` + +### $match + +``` +db.student.aggregate([{$match:{"name.last":"jay"}}]) +``` + +### $limit + +``` +db.student.aggregate({ $limit : 1 }); +``` + +### $skip + +``` +db.student.aggregate({ $skip : 2 }); +``` + +### $unwind + +``` +数据: +db.product.find() +{ "_id" : 1, "item" : "ABC1", "sizes" : [ "S", "M", "L" ] } + +db.product.aggregate( [ { $unwind : "$sizes" } ] ) +``` + +### $group + +``` +数据: +db.student.find() +{ "_id" : ObjectId("58d9ba2d6097167df6313438"), "name" : "jay", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df6313439"), "name" : "ad", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df631343a"), "name" : "ad", "age" : 38 } + +把数据根据age字段进行分组,使用命令: +db.student.aggregate([{$group : {_id : "$age"}}]) +``` + +### $sort + +``` +$sort将输入文档排序后输出,1为按字段升序,-1为降序。 +我们有数据: +db.student.find() +{ "_id" : ObjectId("58d9ba2d6097167df6313438"), "name" : "jay", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df6313439"), "name" : "ad", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df631343a"), "name" : "ad", "age" : 38 } +使用命令: +db.student.aggregate([{$sort : {age :-1}}]) +``` + +### $lookup + +``` +数据: +db.product.insert({"_id":1,"name":"产品1","price":99}) +db.product.insert({"_id":2,"name":"产品2","price":88}) + +db.order.insert({"_id":1,"pid":1,"name":"订单1"}) +db.order.insert({"_id":2,"pid":2,"name":"订单2"}) +db.order.insert({"_id":3,"pid":2,"name":"订单3"}) +db.order.insert({"_id":4,"pid":1,"name":"订单4"}) +db.order.insert({"_id":5,"name":"订单5"}) +db.order.insert({"_id":6,"name":"订单6"}) + +$lookup作左连接组合两个集合的信息使用命令如下: +db.order.aggregate([ + { + $lookup: + { + from: "product", + localField: "pid", + foreignField: "_id", + as: "orderDetail" + } + } + ]) + +``` + +### $geoNear + +``` +一、平面 + +我们使用lng和lat的命名,新建数据如下: +db.places.save({name:"肯德基",loc : { lng: 40.739037, lat: 73.992964 },category:"餐饮"}) +db.places.save({name:"麦当劳",loc : { lng : 42.739037, lat: 73.992964 },category:"餐饮"}) +db.places.save({name:"农行",loc : { lng: 41.739037, lat: 73.992964 },category:"银行"}) +db.places.save({name:"地铁站",loc : { lng: 40.639037, lat: 73.992964 },category:"交通"}) + +2d索引创建方式如下: +db.places.createIndex( { "loc": "2d" } ) + + +我们现在需要查到地铁站附近的文档信息,可以使用: +db.places.find({loc : {$near : { lng: 40.639037, lat:73.992964 }}}) + + + +aggregate和$geoNear能指定到范围,比如我们要查范围在坐标值相差2度(平面单位)以内的文档: +db.places.aggregate([ + { + $geoNear: { + spherical:false, + distanceMultiplier:1, + near: { lng: 40.639037, lat:73.992964 }, + distanceField: "dist.distacnce", + maxDistance: 2, + query: { category: "餐饮" }, + includeLocs: "dist.location", + num: 1 + } + } +]) + +二、球面 +新建数据如下: +db.places.save({name:"肯德基",loc : { type: "Point", coordinates: [ 40.639037, 73.992964 ] },category:"餐饮"}) +db.places.save({name:"麦当劳",loc : { type: "Point", coordinates: [ 42.739037, 73.992964 ] },category:"餐饮"}) +db.places.save({name:"农行",loc : { type: "Point", coordinates: [ 41.739037, 73.992964 ] },category:"银行"}) +db.places.save({name:"地铁站",loc : { type: "Point", coordinates: [ 40.639037, 73.992964 ] },category:"交通"}) + +创建2dsphere 索引使用代码: +db.places.createIndex( { loc : "2dsphere" } ) + +$geoNear用于输出某一地理位置2公里内的文档代码如下: +db.places.aggregate([ + { + $geoNear: { + spherical: true, + near: { type: "Point", coordinates: [ 40.639037, 73.992964 ] }, + distanceField: "dist.distacnce", + maxDistance: 2000, + query: { category:"餐饮" }, + includeLocs: "dist.location", + num: 5 + } + } +]) +``` + +### $group和管道表达式 + +``` +测试数据准备: +db.product.insert({"_id":1,"name":"产品1","price":99,"type":"服装"}) +db.product.insert({"_id":2,"name":"产品2","price":88,"type":"服装"}) +db.product.insert({"_id":3,"name":"产品3","price":29,"type":"饰品"}) +db.product.insert({"_id":4,"name":"产品4","price":78,"type":"服装"}) +db.product.insert({"_id":5,"name":"产品5","price":9,"type":"饰品"}) +db.product.insert({"_id":6,"name":"产品6","price":18,"type":"饰品"}) + + +db.product.aggregate([{$group : {_id : "$type", price : {$sum : "$price"}}}]) # 求和$sum +db.product.aggregate([{$group : {_id : "$type", price : {$avg : "$price"}}}]) # 平均值$avg +db.product.aggregate([{$group : {_id : "$type", price : {$min : "$price"}}}]) # 最小值$min +db.product.aggregate([{$group : {_id : "$type", price : {$max : "$price"}}}]) # 最大值$max +db.product.aggregate([{$group : {_id : "$type", tags : {$push : "$name"}}}]) # 数组添加$push +db.product.aggregate([{$group : {_id : "$type", tags : {$addToSet : "$name"}}}])# 数组添加$addToSet +db.product.aggregate([{$group : {_id : "$type", product : {$first : "$name"}}}])# 首元素$first +db.product.aggregate([{$group : {_id : "$type", product : {$last : "$name"}}}]) # 尾元素$last +``` + +### 复合使用示例 + +``` +数据如下: +{ "_id" : ObjectId("58d9b6afa159504ca6c572e0"), "name" : "jay", "age" : 28, "company" : "google", "name" : "a2" } +{ "_id" : ObjectId("58d9ba2d6097167df6313438"), "name" : "jay", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df6313439"), "name" : "ad", "age" : 14 } +{ "_id" : ObjectId("58d9ba2d6097167df631343a"), "name" : "ad", "age" : 38 } +{ "_id" : ObjectId("58d9ba2d6097167df631343b"), "name" : "ad", "age" : 24 } +{ "_id" : ObjectId("58d9ba2e6097167df631343c"), "name" : "ab", "age" : 14 } + +使用代码如下: +db.student.aggregate( + [ + { $match: { age:{"$gt":13} } }, + { $sort: { age: 1 } }, + { $limit: 2 }, + { $group: { _id: "$age", "人数": { $sum: 1 } } }, + ] +) + +aggregate默认返回分组信息_id,如果要去掉,可使用$project。 +因为aggregate是按顺序处理的管道阶段操作器,所以管道的排序也很重要,下面两端代码使用相同的管道阶段操作器,但是不同的顺序,实现的效果也是不同的: +代码一: +db.student.aggregate( + [ + { $match: { age:{"$gt":13} } }, + { $sort: { age: 1 } }, + { $limit: 2 }, + { $group: { _id: "$age", "人数": { $sum: 1 } } } + ] +) + + +代码二: +db.student.aggregate( + [ + { $match: { age:{"$gt":13} } }, + { $group: { _id: "$age", "人数": { $sum: 1 } } }, + { $sort: { _id: 1 } }, + { $limit: 2 } + ] +) +``` + +## 7.性能监控 + +- db.stats(),查看当前数据库的状态,可看到数据库的名称,集合(表)数量,索引数量、大小,数据文件大小,存储空间大小和物理文件大小。 + +- db.serverCmdLineOpts(),查看启动参数 + +- db.serverStatus(),查询MongoDB的服务状态,有助于了解诊断和性能分析 + +``` +serverStatus命令是静态的监控,MongoDB提供了动态的监控执行工具mongostat。 + +db.serverStatus().host # 主机名 +db.serverStatus().locks # 锁信息 +db.serverStatus().globalLock # 全局锁信息 +db.serverStatus().mem # 内存信息 +db.serverStatus().connections # 连接数信息 +db.serverStatus().extra_info # 额外信息 +db.serverStatus().indexCounters # 索引统计信息 +db.serverStatus().backgroundFlushing # 后台刷新信息 +db.serverStatus().cursors # 游标信息 +db.serverStatus().network # 网络信息 +db.serverStatus().repl # 副本集信息 +db.serverStatus().opcountersRepl # 副本集的操作计数器 +db.serverStatus().opcounters # 操作计数器 +db.serverStatus().asserts # 断言信息Asserts +db.serverStatus().writeBacksQueued # writeBacksQueued +db.serverStatus().dur # 持久化(dur) +db.serverStatus().recordStats # 记录状态信息 +db.serverStatus( { workingSet: 1 } ).workingSet # 工作集配置 +db.serverStatus().metrics # 指标信息metrics +``` + +- mongostat,会动态输出一些serverStatus提供的重要信息,每秒输出一次。 + +``` +mongostat会动态输出一些serverStatus提供的重要信息,每秒输出一次。 + +insert query update delete getmore command flushes mapped vsize res faults qrw arw net_in net_out conn time + *0 *0 *0 *0 0 11|0 0 0B 237M 15.0M 0 0|0 0|0 1.50k 22.7k 1 May 1 14:24:25.425 + *0 *0 *0 *0 0 12|0 0 0B 237M 15.0M 0 0|0 0|0 1.55k 23.5k 1 May 1 14:24:26.405 + *0 *0 *0 *0 0 11|0 0 0B 237M 15.0M 0 0|0 0|0 1.52k 23.0k 1 May 1 14:24:27.405 + *0 *0 *0 *0 0 12|0 0 0B 237M 15.0M 0 0|0 0|0 1.52k 23.1k 1 May 1 14:24:28.404 + *0 *0 *0 *0 0 11|0 0 0B 237M 15.0M 0 0|0 0|0 1.52k 23.0k 1 May 1 14:24:29.404 + *0 *0 *0 *0 0 12|0 0 0B 237M 15.0M 0 0|0 0|0 1.53k 23.2k 1 May 1 14:24:30.396 + + +insert、query、update、delete、getmore、command 每种对应操作的发生次数。 +其中faults表示访问失败数,数据从内存交换出去,放到swap。值越小越好,最好不要大于100。 + +faults:查询从磁盘读取数据,标志服务器未达到最佳,所需的数据并未完全保存找内存中 +qr/qw:队列等待的数目。 +ar/aw:活动客户端的数目。 +conn:打开的连接数。 +flushes:数据刷写到磁盘的数目。 +vsize:使用虚拟内存大小。 +mapped:隐射的内存大小,约等于数据目录大小。 +res:实际使用的内存大小 +netId:表示通过网络传输进来的字节数。 +netou:t表示通过网络传输出的字节数。 +time:表示统计的时间。 +``` + +- mongotop,查看哪个几个最繁忙 + +- db.currentOP(),可看到当前执行的进程,类似MySQL的show processlist + +- show profile + +``` +db.setProfilingLevel(2) # enable profiling +db.system.profile.find() # show raw profile entries +show profile # 该命令可以查看最近的5条记录 +该命令可以查看最近的5条记录: +ts:该命令在何时执行。 +millis:执行耗时,以毫秒为单位。 +op:什么操作。 +query:设置的查询条件。 +nReturned:返回的条数。 +docsExamined:文档扫描条数。 + +# 清空还原profile集合使用命令: +db.setProfilingLevel(0) +db.system.profile.drop() +db.createCollection("system.profile", { capped: true, size:4000000 } ) +db.setProfilingLevel(1) +``` + +## 8.备份和恢复 + +### 1).数据库备份和恢复 + +- mongodump导出数据库中全部的内容,使用-q参数增加查询条件,注意-q参数值的标点符号,否则会报错 too many positional arguments + +``` +mongodump --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -o /home +mongodump --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c user -q "{name:'joe'}" -o /home +``` + +- mongorestore使用的数据文件就是mongodump备份的数据文件 + +``` +# 使用/home路径下的BSON和JSON文件恢复数据库test,--drop参数表示如果已经存在test数据库则删除原数据库,去掉--drop则与原数据库合并 +mongorestore --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test /home/test --drop +``` + +### 2).表的备份和恢复 + +- 导出json格式的备份文件,从test数据库中student集合到/home/test.json文件中 + +``` +mongoexport --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c student -o /home/test.json +mongoexport --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c student -q "{name: 'joe'}" -o /home/test.json +``` + +- 导出csv格式的备份文件 + +``` +# -f参数用于指定只导致id和name以及age字段,因为csv是表格类型的,所以对于内嵌文档太深的数据导出效果不是很好,所以一般来说会指定某些字段导出。 +mongoexport --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c student --csv -f _id,name,age -o /home/test.csv +``` + +- json格式导入,使用备份文件/home/test.json导入数据到test数据库的student集合中 + +``` +# --upsert表示更新现有数据,如果不使用—upsert则导入时已经存在的文档会报_id重复,数据不再插入。也可以使用--drop删除原数据。 +mongoimport --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c student /home/test.json --upsert +``` + +- csv格式导入,--headerline指明不导入第一行,csv格式的文件第一行为列名 + +``` +mongoimport --port 22400 -u "root" -p "123456" --authenticationDatabase "admin" -d test -c student --type csv --headerline --file /home/test.csv +``` diff --git a/orm/src/test/java/com/zfoo/orm/client/SingleMongoTest.java b/orm/src/test/java/com/zfoo/orm/client/SingleMongoTest.java new file mode 100644 index 00000000..ca8f1066 --- /dev/null +++ b/orm/src/test/java/com/zfoo/orm/client/SingleMongoTest.java @@ -0,0 +1,374 @@ +package com.zfoo.orm.client; + +import com.mongodb.BasicDBObject; +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.client.*; +import com.mongodb.client.model.Accumulators; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Filters; +import com.zfoo.protocol.util.StringUtils; +import com.zfoo.scheduler.util.TimeUtils; +import org.bson.Document; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import static com.mongodb.client.model.Filters.eq; + +/** + * @author jaysunxiao + * @version 1.0 + * @since 2018-07-11 12:20 + */ +@Ignore +public class SingleMongoTest { + + // To connect to mongodb server + private MongoClient mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017)))) +// .applyToConnectionPoolSettings(builder -> builder.applySettings(ConnectionPoolSettings.builder().maxSize(300).build())) +// .credential(MongoCredential.createCredential("root", "admin", "123456".toCharArray())) + .build()); + + // If MongoDB in secure mode, authentication is required. + // Now connect to your databases + private MongoDatabase mongodb = mongoClient.getDatabase("test"); + + @Test + public void findTest() { + System.out.println("Connect to database successfully!"); + System.out.println("MongoDatabase inof is : " + mongodb.getName()); + + System.out.println("当前数据库中的所有集合是:"); + for (String name : mongodb.listCollectionNames()) { + System.out.println(name); + } + + // 查找并且遍历集合student的所有文档 + MongoCollection collection = mongodb.getCollection("student"); + System.out.println("Collection created successfully"); + collection.find().forEach((Consumer) document -> System.out.println(document)); + } + + @Test + public void findEqTest() { + var collection = mongodb.getCollection("student"); + collection.find(eq("_id", 1)).forEach((Consumer) document -> System.out.println(document)); + } + + @Test + public void findPatternTest() { + var pattern = Pattern.compile("jay"); + var query = new BasicDBObject("name", pattern); + var collection = mongodb.getCollection("student"); + collection.find(query).forEach((Consumer) document -> System.out.println(document)); + } + + @Test + public void aggregateTest() { + var collection = mongodb.getCollection("student"); + // likeNum中有9的文档根据age分组后统计数量。 + var result = collection.aggregate(Arrays.asList( + Aggregates.match(Filters.eq("name", "jay1")), + Aggregates.group("$age", Accumulators.sum("count", 1))) + ); + System.out.println(result.first()); + } + + + @Test + public void insertOneTest() { + // 查找并且便利集合student的所有文档 + MongoCollection collection = mongodb.getCollection("student"); + + // 插入一个文档 + Document document = new Document("_id", 1) + .append("name", "hello mongodb"); + collection.insertOne(document); + + collection.find().forEach((Consumer) doc -> System.out.println(doc.toJson())); + } + + @Test + public void insertManyTest() { + // 查找并且便利集合student的所有文档 + MongoCollection collection = mongodb.getCollection("student"); + + //插入数据 + var datas = new ArrayList(); + for (int i = 0; i < 10; i++) { + var studentName = "jay" + i; + var document = new Document("name", studentName).append("email", studentName + "@zfoo.com").append("age", i); + datas.add(document); + } + collection.insertMany(datas); + // 查询年龄大于等于20,小于25 + FindIterable result = collection.find(new Document("age", new Document("$gte", 20).append("$lt", 25))); + System.out.println(result.first()); + } + + @Test + public void updateOneTest() { + // 查找并且便利集合student的所有文档 + MongoCollection collection = mongodb.getCollection("student"); + + // 更新一个文档,updateOne方法第一个参数是查询条件,如果查出多条也只修改第一条;第二个参数是修改条件。 + collection.updateOne(eq("age", 10), new Document("$set", new Document("name", "new hello mongodb"))); + + collection.find().forEach((Consumer) doc -> System.out.println(doc.toJson())); + } + + + @Test + public void updateManyTest() { + // 查找并且便利集合student的所有文档 + MongoCollection collection = mongodb.getCollection("student"); + + // 更新一个文档 + collection.updateMany(eq("age", 10), new Document("$set", new Document("name", "new hello mongodb"))); + + collection.find().forEach((Consumer) doc -> System.out.println(doc.toJson())); + } + + @Test + public void deleteTest() { + MongoCollection collection = mongodb.getCollection("student"); + + // 删除一个文档 + collection.deleteOne(eq("_id", 1)); + + collection.find().forEach((Consumer) doc -> System.out.println(doc.toJson())); + } + + @Test + public void mapReduceTest() { + String mapFunction = "" + + "function() { " + + "emit(this.name, { count: 1 });" + + "}"; + String reduceFunction = "" + + "function(key, values) {" + + "var reduced = { name:key, count:0 };" + + "values.forEach(function(value){ reduced.count += value.count; }); " + + "return reduced;" + + "}"; + MongoCollection collection = mongodb.getCollection("student"); + var iterable = collection.mapReduce(mapFunction, reduceFunction); + iterable.forEach((Consumer) document -> System.out.println(document)); + } + + + // ****************************************************************************************************** + // 更新一个字段和更新多个字段测试,测试结果,区别不大,可以忽略 + + + // 耗时[42598] + @Test + public void updateAllFieldTest() { + var startTime = TimeUtils.currentTimeMillis(); + var count = 10_0000; + var mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings(builder -> + builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017)))) + .build()); + + var mongodb = mongoClient.getDatabase("test"); + var collection = mongodb.getCollection("student"); + for (var j = 0; j < count; j++) { + var key = j; + var value = j; + try { + // 插入一个文档 + Document document = new Document("_id", key) + .append("name0", value) + .append("name1", value) + .append("name2", value) + .append("name3", value) + .append("name4", value) + .append("name5", value) + .append("name6", value) + .append("name7", value) + .append("name8", value) + .append("name9", value) + .append("name10", value); + collection.updateOne(eq("_id", key), new Document("$set", document)); +// collection.insertOne(document); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + var endTime = TimeUtils.currentTimeMillis(); + System.out.println(StringUtils.format("耗时[{}]", endTime - startTime)); + } + + // 耗时[41971] + @Test + public void updateOneFieldTest() { + var startTime = TimeUtils.currentTimeMillis(); + var count = 10_0000; + var mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings(builder -> + builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017)))) + .build()); + var mongodb = mongoClient.getDatabase("test"); + var collection = mongodb.getCollection("student"); + for (var j = 0; j < count; j++) { + var key = j; + var value = j; + try { + // 插入一个文档 + Document document = new Document("_id", key) + .append("name0", value); + collection.updateOne(eq("_id", key), new Document("$set", document)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + var endTime = TimeUtils.currentTimeMillis(); + System.out.println(StringUtils.format("耗时[{}]", endTime - startTime)); + } + + + // ****************************************************************************************************** + // wiredTiger引擎测试 + // inMemory引擎测试 + private static final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 10); + + /** + * inMemory测试结果如下: + * 总线程数[1],操作数[10000],耗时[4212] + * 总线程数[1],操作数[1000000],耗时[394215] + * 总线程数[4],操作数[1000000],耗时[223355] + * 总线程数[8],操作数[1000000],耗时[297476] + * 总线程数[16],操作数[1000000],耗时[442782] + * 总线程数[32],操作数[1000000],耗时[713675] + *

+ *

+ * wiredTiger测试结果如下: + * 总线程数[1],操作数[10000],耗时[4170] + * 总线程数[1],操作数[1000000],耗时[400208] + * 总线程数[4],操作数[1000000],耗时[230471] + * 总线程数[8],操作数[1000000],耗时[298409] + */ + @Test + public void mongodbWriteTest() { + var startTime = TimeUtils.currentTimeMillis(); + var threadNum = 8; + var count = 100_0000; + var list = new ArrayList(); + for (var i = 0; i < threadNum; i++) { + var flag = new AtomicBoolean(false); + list.add(flag); + + var mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings(builder -> + builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017)))) + .build()); + var mongodb = mongoClient.getDatabase("test"); + var collection = mongodb.getCollection("student"); + executor.execute(new Runnable() { + @Override + public void run() { + for (var j = 0; j < count; j++) { + var key = j; + var value = j; + try { + // 插入一个文档 + Document document = new Document("_id", key) + .append("name", value); + collection.updateOne(eq("_id", key), new Document("$set", document)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + flag.set(true); + } + }); + } + + while (true) { + var flag = list.stream().anyMatch(it -> it.get() == false); + if (flag) { + continue; + } else { + break; + } + } + + var endTime = TimeUtils.currentTimeMillis(); + System.out.println(StringUtils.format("总线程数[{}],操作数[{}],耗时[{}]" + , threadNum, count, endTime - startTime)); + } + + + /** + * 总线程数[1],操作数[10000],耗时[4252] + * 总线程数[1],操作数[1000000],耗时[407131] + * 总线程数[4],操作数[1000000],耗时[226040] + * 总线程数[8],操作数[1000000],耗时[297418] + * 总线程数[16],操作数[1000000],耗时[436799] + * 总线程数[32],操作数[1000000],耗时[716651] + */ + @Test + public void mongodbReadTest() { + var startTime = TimeUtils.currentTimeMillis(); + var threadNum = 32; + var count = 100_0000; + var list = new ArrayList(); + for (var i = 0; i < threadNum; i++) { + var flag = new AtomicBoolean(false); + list.add(flag); + + var mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyToClusterSettings(builder -> + builder.hosts(Arrays.asList(new ServerAddress("localhost", 27017)))) + .build()); + var mongodb = mongoClient.getDatabase("test"); + var collection = mongodb.getCollection("student"); + executor.execute(new Runnable() { + @Override + public void run() { + for (var j = 0; j < count; j++) { + try { + collection.find(eq("_id", j)).forEach((Consumer) document -> { + if (document == null) { + + } + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + flag.set(true); + } + }); + } + + while (true) { + var flag = list.stream().anyMatch(it -> it.get() == false); + if (flag) { + continue; + } else { + break; + } + } + + var endTime = TimeUtils.currentTimeMillis(); + System.out.println(StringUtils.format("总线程数[{}],操作数[{}],耗时[{}]" + , threadNum, count, endTime - startTime)); + } + +} diff --git a/orm/src/test/java/com/zfoo/orm/client/Student.java b/orm/src/test/java/com/zfoo/orm/client/Student.java new file mode 100644 index 00000000..9e53a0cc --- /dev/null +++ b/orm/src/test/java/com/zfoo/orm/client/Student.java @@ -0,0 +1,38 @@ +package com.zfoo.orm.client; + + +/** + * @author jaysunxiao + * @version 1.0 + * @since 2019-05-01 23:34 + */ +public class Student { + + private long id; + private String name; + + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Student{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +}