数据库概念&环境搭建

目标

  • 能够安装数据库软件
  • 能够知道集合、文档的概念
  • 能够使用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=黑马程序员'
*/