数据库概念mongodb使用数据库CLUD操作
数据库概念&环境搭建
目标
- 能够安装数据库软件
- 能够知道集合、文档的概念
- 能够使用mongoose创建集合的方法创建集合
为什么要使用数据库(★★★)
- 动态网站中的数据都是存储在数据库中的
- 数据库可以用来持久存储客户端通过表单收集的用户信息
- 数据库软件本身可以对数据进行高效的管理
什么是数据库(★★★)
- 数据库即存储数据的仓库,可以将数据进行有序的分门别类的存储。它是独立于语言之外的软件,可以通过API去操作它。
- 常见的数据库软件有:mysql、mongoDB、oracle。
MongoDB数据库下载安装(★★★)
**下载地址:**https://www.mongodb.com/download-center/community
[外链图片转存失败(img-HRsJ9Brw-1568462739111)(images/mongodb.png)]
MongoDB可视化软件
MongoDB可视化操作软件,是使用图形界面操作数据库的一种方式。
[外链图片转存失败(img-xF7kGxMN-1568462739112)(images/sql-keshihua.png)]
数据库相关概念(★★★)
在一个数据库软件中可以包含多个数据仓库,在每个数据仓库中可以包含多个数据集合,每个数据集合中可以包含多条文档(具体的数据)。
术语 | 解释说明 |
---|---|
database | 数据库,mongoDB数据库软件中可以建立多个数据库 |
collection | 集合,一组数据的集合,可以理解为JavaScript中的数组 |
document | 文档,一条具体的数据,可以理解为JavaScript中的对象 |
field | 字段,文档中的属性名称,可以理解为JavaScript中的对象属性 |
Mongoose第三方包
- 使用Node.js操作MongoDB数据库需要依赖Node.js第三方包mongoose
- 使用
npm install mongoose
命令下载
启动MongoDB
在命令行工具中运行net start mongoDB
即可启动MongoDB,否则MongoDB将无法连接。
MongoDB增删改查(CLUD)操作
目标
- 能够对数据库中的数据进行增删改查操作
数据库连接(★★★)
使用mongoose提供的connect方法即可连接数据库。
// 引入mongoose第三方模块 用来操作数据库
const mongoose = require('mongoose');
// 数据库连接
mongoose.connect('mongodb://localhost/playground', { useNewUrlParser: true})// 连接成功.then(() => console.log('数据库连接成功'))// 连接失败.catch(err => console.log(err, '数据库连接失败'));
创建集合(创建表)(★★★)
创建集合分为两步,一是对对集合设定规则,二是创建集合,创建mongoose.Schema构造函数的实例即可创建集合。
// 设定集合规则const courseSchema = new mongoose.Schema({name: String,author: String,isPublished: Boolean});// 创建集合并应用规则const Course = mongoose.model('Course', courseSchema); // courses
创建文档(插入数据)(★★★)
创建文档实际上就是向集合中插入数据。
分为两步:
- 创建集合实例。
- 调用实例对象下的save方法将数据保存到数据库中。
// 创建集合实例const course = new Course({name: 'Node.js course',author: '黑马讲师',tags: ['node', 'backend'],isPublished: true});// 将数据保存到数据库中course.save();
插入数据另外一种方法
//写法一
Course.create({name: 'JavaScript基础', author: '黑马讲师', isPublish: true}, (err, doc) => { // 错误对象console.log(err)// 当前插入的文档console.log(doc)
});
//写法二
Course.create({name: 'JavaScript基础', author: '黑马讲师', isPublish: true}).then(doc => console.log(doc)).catch(err => console.log(err))
mongoDB数据库导入数据
找到mongodb数据库的安装目录,将安装目录下的bin目录放置在环境变量中
[外链图片转存失败(img-IEu9Td2C-1568462739114)(images/mongodb-path.png)]
mongoimport –d 数据库名称 –c 集合名称 –-file 要导入的数据文件
mongoimport -d test -c users --file 数据文件的路径
查询文档(数据)(★★★)
利用find的方法查询
查询所有(★★★)
//定义好表规则
const user = new mongodb.Schema({_id: String,username: String,password: Number,age: Number,name: String,hobbies: [String],email: String
});
//生成对应的对象索引
const userEntity = mongodb.model('User', user);
// 根据条件查找文档(条件为空则查找所有文档)
Course.find().then(result => console.log(result))
// 返回文档集合
[{_id: 5c0917ed37ec9b03c07cf95f,name: 'node.js基础',author: '黑马讲师‘
},{_id: 5c09dea28acfb814980ff827,name: 'Javascript',author: '黑马讲师‘
}]
根据条件查询(★★★)
//定义好表规则
const user = new mongodb.Schema({_id: String,username: String,password: Number,age: Number,name: String,hobbies: [String],email: String
});
//生成对应的对象索引
const userEntity = mongodb.model('User', user);
// 根据条件查找文档
userEntity.findOne({name: 'node.js基础'}).then(result => console.log(result))// 返回文档{_id: 5c0917ed37ec9b03c07cf95f,name: 'node.js基础',author: '黑马讲师‘
}
多条件查询(★★)
根据范围查询
// 匹配大于 小于userEntity.find({age: {$gt: 20, $lt: 50}}).then(result => console.log(result))
####包含某字符
// 匹配包含userEntity.find({hobbies: {$in: ['敲代码']}}).then(result => console.log(result))
查询某一个字段
// 选择要查询的字段
userEntity.find().select('name email').then(result => console.log(result))
排序查询
// 将数据按照年龄进行排序 升序userEntity.find().sort('age').then(result => console.log(result))// 将数据按照年龄进行排序 降序userEntity.find().sort('-age').then(result => console.log(result))
分页查询
// skip 跳过多少条数据 limit 查询几条数据userEntity.find().skip(2).limit(2).then(result => console.log(result))
删除文档(★★★)
删除单条数据
//定义好表规则
const user = new mongodb.Schema({_id: String,username: String,password: Number,age: Number,name: String,hobbies: [String],email: String
});
//生成对应的对象索引
const userEntity = mongodb.model('User', user);
// 删除单个
userEntity.findOneAndDelete({}).then(result => console.log(result))
删除多个数据
//定义好表规则
const user = new mongodb.Schema({_id: String,username: String,password: Number,age: Number,name: String,hobbies: [String],email: String
});
//生成对应的对象索引
const userEntity = mongodb.model('User', user);
// 删除多个
userEntity.deleteMany({}).then(result => console.log(result))
更新文档(★★★)
更新单条数据
// 更新单个
User.updateOne({查询条件}, {要修改的值}).then(result => console.log(result))
更新多条数据
// 更新多个
User.updateMany({查询条件}, {要更改的值}).then(result => console.log(result))
mongoose验证(★★)
在创建集合规则时,可以设置当前字段的验证规则,验证失败就则输入插入失败。说白了,就是规定我们插入数据库每一项的条件
- required: true 必传字段
- minlength:3 字符串最小长度
- maxlength: 20 字符串最大长度
- min: 2 数值最小为2
- max: 100 数值最大为100
- enum: [‘html’, ‘css’, ‘javascript’, ‘node.js’]
- trim: true 去除字符串两边的空格
- validate: 自定义验证器
- default: 默认值
代码示例
const postSchema = new mongoose.Schema({title: {type: String,// 必选字段required: [true, '请传入文章标题'],// 字符串的最小长度minlength: [2, '文章长度不能小于2'],// 字符串的最大长度maxlength: [5, '文章长度最大不能超过5'],// 去除字符串两边的空格trim: true},age: {type: Number,// 数字的最小范围min: 18,// 数字的最大范围max: 100},publishDate: {type: Date,// 默认值default: Date.now},category: {type: String,// 枚举 列举出当前字段可以拥有的值enum: {values: ['html', 'css', 'javascript', 'node.js'],message: '分类名称要在一定的范围内才可以'}},author: {type: String,validate: {validator: v => {// 返回布尔值// true 验证成功// false 验证失败// v 要验证的值return v && v.length > 4},// 自定义错误信息message: '传入的值不符合验证规则'}}
});
如何捕获验证的错误信息
.catch(error => {// 获取错误信息对象const err = error.errors;// 循环错误信息对象for (var attr in err) {// 将错误信息打印到控制台中console.log(err[attr]['message']);}})
集合关联(★★)
通常不同集合的数据之间是有关系的,例如文章信息和用户信息存储在不同集合中,但文章是某个用户发表的,要查询文章的所有信息包括发表用户,就需要用到集合关联。
- 使用id对集合进行关联
- 使用populate方法进行关联集合查询
[外链图片转存失败(img-xGEmEFdm-1568462739119)(images/关联查询.png)]
示例demo
// 用户集合
const User = mongoose.model('User', new mongoose.Schema({ name: { type: String }
}));
// 文章集合
const Post = mongoose.model('Post', new mongoose.Schema({title: { type: String },// 使用ID将文章集合和作者集合进行关联author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
}));
const userEntity = mongodb.model('User', user);
const postEntity = mongodb.model('Post', title);
//1.插入数据的时候需要关联起来
userEntity.create({ name: '王五' }).then(result => console.log(result));
postEntity.create({ title: '射雕英雄传', author: '5c95ac7d18f84d8fac8ee4e1' }).then(resule => console.log(result));
//联合查询
postEntity.find().populate('author').then((err, result) => console.log(result));
用户信息增删改查(★★★)
功能
搭建网站服务器,实现客户端与服务器端的通信
连接数据库,创建用户集合,向集合中插入文档
当用户访问/list时,将所有用户信息查询出来
将用户信息和表格HTML进行拼接并将拼接结果响应回客户端
当用户访问/add时,呈现表单页面,并实现添加用户信息功能
当用户访问/modify时,呈现修改页面,并实现修改用户信息功能
当用户访问/delete时,实现用户删除功能
步骤
1.创建服务器
const http = require('http');
const app = http.createServer();
2.引入mongodb模块,链接数据库
const mongoose = require('mongoose');
// 数据库连接 27017是mongodb数据库的默认端口
mongoose.connect('mongodb://localhost/playground', { useNewUrlParser: true }).then(() => console.log('数据库连接成功')).catch(() => console.log('数据库连接失败'));
3.创建用户表规则,获取集合
const userSchema = new mongoose.Schema({name: {type: String,required: true,minlength: 2,maxlength: 20},age: {type: Number,min: 18,max: 80},password: String,email: String,hobbies: [ String ]
});// 创建集合 返回集合构造函数
const User = mongoose.model('User', userSchema);
4.导入用户的数据
mongoimport -d test -c users --file 数据文件的路径
5.当用户访问/list时,将所有用户信息查询出来
5.1 实现路由功能,利用 request对象的method来判断是什么请求,然后拿到请求的path路径,根据不同的path路径来实现不同的功能逻辑,
5.2 如果是list,查询用户信息,然后遍历用户数据,拼接获取的数据,渲染到页面
5.3 如果是add,跳转到添加页面
5.4 如果是modify,跳转到用户修改页面,根据用户id查询出用户的所有信息,显示在页面
5.4 如果是remove,根据用户的id,来删除这条数据,然后重新挑换到list页面
5.5 如果是post请求,路径是add,说明用户点击了提交,需要获取用户信息,然后插入到数据库
5.6 如果是post请求,路径是modify,说明用户点击了修改,需要拿到信息,根据用户id来修改数据库里面的数据
// 为服务器对象添加请求事件
app.on('request', async (req, res) => {// 请求方式const method = req.method;// 请求地址const { pathname, query } = url.parse(req.url, true);if (method == 'GET') {// 呈现用户列表页面if (pathname == '/list') {// 查询用户信息let users = await User.find();// html字符串,拼接html的内容let list ='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>用户列表</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"></head><body><div class="container"><h6><a href="/add" class="btn btn-primary">添加用户</a></h6><table class="table table-striped table-bordered"><tr><td>用户名</td><td>年龄</td><td>爱好</td><td>邮箱</td><td>操作</td></tr>';//拿到了用户的信息,用户的信息可能不只一条,所以我们需要去遍历用户信息来拼接里面的内容users.forEach(item => {list += `<tr><td>${item.name}</td><td>${item.age}</td><td>`;//一个用户会有多个爱好,所以需要进行遍历item.hobbies.forEach(item => {list += `<span>${item}</span>`;})list += `</td><td>${item.email}</td><td><a href="/remove?id=${item._id}" class="btn btn-danger btn-xs">删除</a><a href="/modify?id=${item._id}" class="btn btn-success btn-xs">修改</a></td></tr>`;});//拼接HTML页面剩余的标签list += `</table></div></body></html>`;//代码执行到这里,所有的数据已经拼接完毕,需要返回给页面res.end(list);}else if (pathname == '/add') {// 呈现添加用户表单页面let add = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>用户列表</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"></head><body><div class="container"><h3>添加用户</h3><form method="post" action="/add"><div class="form-group"><label>用户名</label><input name="name" type="text" class="form-control" placeholder="请填写用户名"></div><div class="form-group"><label>密码</label><input name="password" type="password" class="form-control" placeholder="请输入密码"></div><div class="form-group"><label>年龄</label><input name="age" type="text" class="form-control" placeholder="请填写邮箱"></div><div class="form-group"><label>邮箱</label><input name="email" type="email" class="form-control" placeholder="请填写邮箱"></div><div class="form-group"><label>请选择爱好</label><div><label class="checkbox-inline"><input type="checkbox" value="足球" name="hobbies"> 足球</label><label class="checkbox-inline"><input type="checkbox" value="篮球" name="hobbies"> 篮球</label><label class="checkbox-inline"><input type="checkbox" value="橄榄球" name="hobbies"> 橄榄球</label><label class="checkbox-inline"><input type="checkbox" value="敲代码" name="hobbies"> 敲代码</label><label class="checkbox-inline"><input type="checkbox" value="抽烟" name="hobbies"> 抽烟</label><label class="checkbox-inline"><input type="checkbox" value="喝酒" name="hobbies"> 喝酒</label><label class="checkbox-inline"><input type="checkbox" value="烫头" name="hobbies"> 烫头</label></div></div><button type="submit" class="btn btn-primary">添加用户</button></form></div></body></html>`;res.end(add);}else if (pathname == '/modify') {//如果是修改页面,呈现修改的页面,修改肯定是修改某一个用户的信息,需要根据用户的id来进行查询let user = await User.findOne({_id: query.id});let hobbies = ['足球', '篮球', '橄榄球', '敲代码', '抽烟', '喝酒', '烫头', '吃饭', '睡觉', '打豆豆']console.log(user)// 呈现修改用户表单页面let modify = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>用户列表</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"></head><body><div class="container"><h3>修改用户</h3><form method="post" action="/modify?id=${user._id}"><div class="form-group"><label>用户名</label><input value="${user.name}" name="name" type="text" class="form-control" placeholder="请填写用户名"></div><div class="form-group"><label>密码</label><input value="${user.password}" name="password" type="password" class="form-control" placeholder="请输入密码"></div><div class="form-group"><label>年龄</label><input value="${user.age}" name="age" type="text" class="form-control" placeholder="请填写邮箱"></div><div class="form-group"><label>邮箱</label><input value="${user.email}" name="email" type="email" class="form-control" placeholder="请填写邮箱"></div><div class="form-group"><label>请选择爱好</label><div>`;hobbies.forEach(item => {// 判断当前循环项在不在用户的爱好数据组let isHobby = user.hobbies.includes(item);if (isHobby) {modify += `<label class="checkbox-inline"><input type="checkbox" value="${item}" name="hobbies" checked> ${item}</label>`}else {modify += `<label class="checkbox-inline"><input type="checkbox" value="${item}" name="hobbies"> ${item}</label>`}})modify += `</div></div><button type="submit" class="btn btn-primary">修改用户</button></form></div></body></html>`;res.end(modify); }else if (pathname == '/remove') {//删除根据用户id进行删除await User.findOneAndDelete({_id: query.id});//通过301的相应码进行重定向res.writeHead(301, {Location: '/list'});res.end();}}else if (method == 'POST') {// 用户添加功能if (pathname == '/add') {// 接受用户提交的信息let formData = '';// 接受post参数,这是一个持续的过程,我们不知道什么时候传递好了,需要在end里面去进行监听req.on('data', param => {formData += param;})// post参数接受完毕req.on('end', async () => {//利用 querystring来进行数据的解析let user = querystring.parse(formData)// 将用户提交的信息添加到数据库中await User.create(user);// 301代表重定向// location 跳转地址res.writeHead(301, {Location: '/list'});res.end();})}else if (pathname == '/modify') {// 接受用户提交的信息let formData = '';// 接受post参数req.on('data', param => {formData += param;})// post参数接受完毕req.on('end', async () => {let user = querystring.parse(formData)// 将用户提交的信息添加到数据库中await User.updateOne({_id: query.id}, user);// 301代表重定向// location 跳转地址res.writeHead(301, {Location: '/list'});res.end();})}}});
// 监听端口
app.listen(3000);
相关模块方法
url模块
专门用来处理请求路径相关
url.parse(urlString,boolean,boolean)
parse这个方法可以将一个url的字符串解析并返回一个url的对象
urlString 传入一个url地址的字符串
第二个参数(可省):如果设为 true
,则返回的 URL 对象的 query
属性会是一个使用 querystring
模块的 parse()
生成的对象。 如果设为 false
,则 query
会是一个未解析未解码的字符串。 默认为 false
。
例如:
[外链图片转存失败(img-ciOqEdko-1568462739120)(images/url-true.png)]
第三个参数(可省):如果设为 true
,则 //
之后至下一个 /
之前的字符串会解析作为 host
。 例如, //foo/bar
会解析为 {host: 'foo', pathname: '/bar'}
而不是 {pathname: '//foo/bar'}
。 默认为 false
。
url.format(urlObj)
format这个方法是将传入的url对象编程一个url字符串并返回
url.format({protocol:"http:",host:"182.163.0:60",port:"60"
});
/*
返回值:
'http://182.163.0:60'
*/
url.resolve(from, to)
resolve这个方法返回一个格式为"from/to"的字符串,把参数进行一个拼接,然后返回
const url = require('url');
url.resolve('/one/two/three', 'four'); // '/one/two/four'
url.resolve('http://example.com/', '/one'); // 'http://example.com/one'
url.resolve('http://example.com/one', '/two'); // 'http://example.com/two'
querystring模块
querystring从字面上的意思就是查询字符串,一般是对http请求所带的数据进行解析
querystring.parse(str,separator,eq,options)
parse这个方法是将一个字符串反序列化为一个对象。
str:指需要反序列化的字符串;
separator(可省):指用于分割str这个字符串的字符或字符串,默认值为"&";
eq(可省):指用于划分键和值的字符或字符串,默认值为"=";
decodeURIComponent:传入一个function,用于对含有%的字符串进行解码,默认值为querystring.unescape,默认是utf-8的编码,如果不是需要进行定义
// 假设 gbkDecodeURIComponent 函数已存在。querystring.parse('w=%D6%D0%CE%C4&foo=bar', null, null,{ decodeURIComponent: gbkDecodeURIComponent });
querystring.stringify(obj,separator,eq,options)
stringify这个方法是将一个对象序列化成一个字符串,与querystring.parse相对。
obj:指需要序列化的对象
separator(可省):用于连接键值对的字符或字符串,默认值为"&";
eq(可省):用于连接键和值的字符或字符串,默认值为"=";
options(可省):传入一个对象,该对象可设置encodeURIComponent这个属性:
encodeURIComponent:值的类型为function,可以将一个不安全的url字符串转换成百分比的形式,默认值为querystring.escape()。
querystring.stringify({name: 'heima', sex: [ 'man', 'women' ] });
/*
return:
'name=heima&sex=man&sex=women'
*/
querystring.escape(str)
escape可使传入的字符串进行编码
querystring.escape("name=黑马程序员");
/*
return:
'name%3D%E9%BB%91%E9%A9%AC%E7%A8%8B%E5%BA%8F%E5%91%98'
*/
querystring.unescape(str)
unescape方法可将含有%的字符串进行解码
querystring.unescape('name%3D%E9%BB%91%E9%A9%AC%E7%A8%8B%E5%BA%8F%E5%91%98');
/*
return:
'name=黑马程序员'
*/
数据库概念mongodb使用数据库CLUD操作相关推荐
- day03-数据库概念mongodb使用数据库CLUD操作
数据库概念&环境搭建 目标 能够安装数据库软件 能够知道集合.文档的概念 能够使用mongoose创建集合的方法创建集合 为什么要使用数据库(★★★) 动态网站中的数据都是存储在数据库中的 数 ...
- NoSQL数据库概念与NoSQL数据库家族
什么是NoSQL数据库? NoSQL数据库即为not noly sql 数据库,意为不仅仅是SQL数据库,泛指非关系型数据库: ----->>> NoSQL 不拘泥于关系型数据库的设 ...
- MongoDB数据库(2.MongoDB对数据库的操作以及Mongodb的增删改查)
MongoDB中对数据库的相关操作 1. 查看当前已有的数库 show dbs 或者 show databases 2.进入数据库 use 数据名 如果没有这个 ...
- MySQL学习笔记01【数据库概念、MySQL安装与使用】
MySQL 文档-黑马程序员(腾讯微云):https://share.weiyun.com/RaCdIwas 1-MySQL基础.pdf.2-MySQL约束与设计.pdf.3-MySQL多表查询与事务 ...
- KingbaseES数据库概念(一)--数据库简介
1. 金仓数据库的发展历史 KingbaseES数据库是人大金仓自主研发的一种通用关系型数据库,产品融合了人大金仓在数据库领域几十年的产品研发和企业级应用经验,可满足各行业用户多种场景的数据处理需求. ...
- MongoDB 删除数据库
MongoDB 删除数据库 语法 MongoDB 删除数据库的语法格式如下: db.dropDatabase() 删除当前数据库,默认为 test,你可以使用 db 命令查看当前数据库名. 实例 以下 ...
- 用python向mongodb插入数据_Python操作MongoDB数据库(一)
Python操作MongoDB数据库(一) 前言 干货文章继续出发!隔的时间有些久了哈,对 MongoDB 安装回顾的同学,可以看下windows环境 <初识 MongoDB 数据库>.这 ...
- 数据库概念 MySQL 库操作 表操作 记录操作
什么是数据(Data)? 数据就是描述事物的符号记录称为数据 什么是数据库(DB)? 数据库就是存放数据的仓库,不过这个仓库是在计算机存储设备上的,而且数据是按一定的格式存放的 数据库管理系统(DBM ...
- python数据库-mongoDB的高级查询操作(55)
一.MongoDB索引 为什么使用索引? 假设有一本书,你想看第六章第六节讲的是什么,你会怎么做,一般人肯定去看目录,找到这一节对应的页数,然后翻到这一页.这就是目录索引,帮助读者快速找到想要的章节. ...
最新文章
- IC/FPGA 技术交流
- C++实现遍历链表一次求出中间的节点
- 存储服务器配置型号,存储服务器配置要求指什么
- 压缩包解压后SecureCRT无法连接的解答
- pthread_cleanup_push()/pthread_cleanup_pop()
- Codeforces Round #402 D String Game(二分)
- c语言 -1%4,**************
- 练字格子纸模板pdf_这么好用的模板,我要好好保存下来!
- (转)淘淘商城系列——在业务逻辑中添加缓存
- 关于ECLIPSE中JSP代码无提示
- Eclipse注释模板设置详解
- 触摸屏下的MFC程序
- openwrt开发--驱动程序IPK包开发(GPIO控制)
- 无心剑中译莎士比亚诗20首
- DAY02,C语言基础编程题
- 马斯克收购 Twitter 后的 Web3 改革方向
- gpio引脚介绍 树莓派3b_树莓派4的GPIO接口介绍
- Pdf.js 解决电子印章问题(最新)
- Dubbo:Dubbo服务发现
- Word02-隐藏回车换行符
热门文章
- linux下硬盘拷贝
- 以人为本中的“本”是指
- 面试官,怎样实现 Router 框架?
- IC-CAD IC 设计流程及 EDA 工具
- 项目管理(PMP)真题解析(二)
- 攀爬Spring珠穆拉玛峰:Spring的启动流程
- 小程序日历控件分享 按月传值显示
- オフショア開発を成功させる工夫10点
- 为什么下载那种小电影时,经常会卡在99%?
- W: GPG 错误:http://nginx.org/packages/ubuntu bionic InRelease: 由于没有公钥,无法验证下列签名: NO_PUBKEY ABF5BD827BD9