Express 使用 MongoDB(mongoose)

182 阅读5分钟

在开发基于 Node.jsExpress 的项目时,使用 Mongoose 是连接和操作 MongoDB 数据库的标准方式。本文将系统性地介绍如何定义模型、查询数据库表。

定义数据模型(Model)

在 Mongoose 中,Schema 用于描述文档结构,Model 则是操作集合的接口。

示例:创建 User 模型(可以理解为 mysql 表)

import mongoose from 'mongoose';

const userSchema = new mongoose.Schema(
  {
    id: { type: Number, required: true, unique: true},
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    age: Number,
    isActive: { type: Boolean, default: true },
    createdAt: { type: Date, default: Date.now }
  },
  { 
    collection: 'users' // 明确指定集合名称(可以理解为 "表" 名称)
  }
);

// 创建并导出 User 模型; 模型名为 User,集合名为 users.
export default mongoose.model('User', userSchema);

通过 Schema 定义了用户文档的结构,并定义了像 name 为必填字符串、 email 为必填字符串、需唯一、 isActive 默认为truecreatedAt自动记录创建时间。

导出 mongoose.model('User', userSchema) 则是操作数据库集合(增、删、改、查)。

注:User 第一次被执行时,若 users 集合不存在时,会自动创建集合。

实现数据库功能 users

你可以通过 Mongoose 提供的方法对集合进行查询。

注:下方代码为封装的路由,如不理解请看 Express 路由

  1. 新增用户 save()
// src/router/api/user.js
import express from 'express';
import User from '../../database/User.js';

const router = express.Router();

router.post('/add', async (req, res, next) => {
  const { name, email, age } = req.body;
    
   // 判断用户名,不存在则把问题给到错误处理中间件
   if (!name) {
     next(new Error('用户名不能为空'));
   }
   // 判断邮箱,不存在则把问题给到错误处理中间件
   if (!email) {
     next(new Error('邮箱不能为空'));
   }

  try {
    // 创建 User 模型数据
    const newUser = new User({
      id: 1,  // 这里的 id 自增逻辑需要自己实现(学习时可忽略)。
      name,
      email,
      age,
      isActive: true
    });
    
    // 保存新增用户
    await newUser.save();

    res.status(200).json({
      code: 0,
      message: '用户创建成功',
      data: newUser
    });
  } catch (error) {
    const err = new Error('邮箱已存在');
    err.statusCode = 400;
    next(err);
  }
});

export default router;

2. 查询所有用户 find()

// src/router/api/user.js
import express from 'express';
import User from '../../database/User.js';

const router = express.Router();

router.get('/list', async (req, res, next) => {
  try {
    // 查询所有用户
    const users = await User.find(); 
    // 返回 JSON 数据
    res.json({
      code: 0,
      data: users,
    });
  } catch (error) {
    // 如出现错误,交给错误处理中间件进行处理。
    next(new Error('获取用户列表失败'))
  }
});

export default router;

3. 通过 ID 查询指定用户 findOne()

// src/router/api/user.js
import express from 'express';
import User from '../../database/User.js';

const router = express.Router();

router.get('/:id', async (req, res, next) => {
  const { params } = req;
  try {
    // 通过 findOne 方法查找 id 为 * 的用户并返回
    // findOne() 的参数是一个对象,你可以使用多个字段进行查询,它会返回第一个匹配的文档。
    const users = await User.findOne({ id: Number(params.id)});
    if (!users) {
      const err = new Error('用户不存在');
      err.statusCode = 400;
      next(err);
    }
    res.json({
      code: '0',
      data: users,
    });
  } catch (error) {
    console.log(error);
  }
});

export default router;

4.通过 ID 修改指定用户 findOne()

// src/router/api/user.js
import express from 'express';
import User from '../../database/User.js';

const router = express.Router();

router.put('/update', async (req, res, next) => {
  const { id, name, email, age } = req.body;

  const users = await User.findOne({ id });
  if (!users) {
    const err = new Error('用户不存在');
    err.statusCode = 400;
    next(err);
  }

  users.name = name;
  users.email = email;
  users.age = age;
  
  // 已经通过 const users = await User.findOne({ id }); 获取了一个用户文档的实例; 
  // 应该使用变量 users 调用 .save() 方法来保存更改
  await users.save();

  res.json({ code: '0', message: '更新成功', data: users });
});

export default router;

5.** 通过 ID 删除指定用户 ** remove()

import express from 'express';
import User from '../../database/User.js';

const router = express.Router();

router.delete('/delete', async (req, res, next) => {
  const { id } = req.body;
  const users = await User.findOne({ id });
  if (!users) {
    const err = new Error('用户不存在');
    err.statusCode = 400;
    next(err);
  }
  // 删除用户
  await users.remove();
  res.json({ code: '0', message: '删除成功' });
});

export default router;
在 EJS 模板中实现数据库操作

注:这里需要用到响应动态页面,如果不了解请看 Express 响应动态页面 EJS 模板引擎

  1. 模板文件:user-list.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><%= title %></title>
</head>
<body>
    <!-- 引入公共文件 -->
    <%- include('includes/header.html') %>
    <div class="content">
        <% if (list.length > 0) { %>
        <ul class="list">
            <% list.forEach((item, index) => { %>
            <li class="active">
                <p><%= item.name %> - <%= item.age %> - <%= item.email %></p>
                <div>
                    <button class="edit" onclick="setUser('<%= encodeURIComponent(JSON.stringify(item)) %>')">修改</button>
                    <button class="delete" onclick="deleteUser('<%= item.id %>')">删除</button>
                </div>
            </li>
            <% }) %>
        </ul>
        <% } else { %>
        <p class="noData">暂无数据</p>
        <% } %>
    </div>
    <button class="add" onclick="setUser()">创建用户</button>
    <!--  新增 / 修改  -->
    <div id="addForm" class="addForm">
        <h3>
            <b id="title">新增用户</b>
            <span onclick="document.getElementById('addForm').style.display = 'none'">关闭</span>
        </h3>
        <form id="userForm">
            <input type="hidden" name="id" value="" />
            <input type="text" name="name" placeholder="姓名" required />
            <input type="email" name="email" placeholder="邮箱" required />
            <input type="number" name="age" placeholder="年龄" />
            <button class="add" type="submit">提交</button>
        </form>
    </div>
</body>

<script>
    document.getElementById('userForm').addEventListener('submit', async function (e) {
        e.preventDefault();
        const formData = new FormData(this);
        const data = Object.fromEntries(formData);
        let res;
        if (data.id) {
            res = await fetch('/user/update', {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            });
        } else {
            res = await fetch('/user/add', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            });
        }
        const result = await res.json();
        if (result.code === '0') {
            alert(`${data.id ? '修改' : '创建'}成功`);
            location.reload();
        }
    });
    async function setUser(data) {
        const user = data ? JSON.parse(decodeURIComponent(data)) : {};
        const addForm = document.getElementById('addForm');
        const title = document.getElementById('title');
        if (user.id) {
            title.innerHTML = '修改用户';
            addForm.style.display = 'block';
            const inputs = document.querySelectorAll('#userForm input');
            inputs.forEach(input => {
                if (input.name) {
                    input.value = user[input.name];
                }
            })
        } else {
            title.innerHTML = '新增用户';
            addForm.style.display = 'block';
            const inputs = document.querySelectorAll('#userForm input');
            inputs.forEach(input => {
                if (input.type === 'text' || input.type === 'email' || input.type === 'number') {
                    input.value = '';
                } else if (input.type === 'hidden') {
                    input.value = '';
                }
            });
        }
    }
    async function deleteUser(id) {
        if (confirm('确定要删除该用户吗?')) {
            await fetch(`/user/delete/${id}`, { method: 'DELETE' });
            location.reload();
        }
    }
</script>
</html>

2. 样式文件 global.css

*,
*:before,
*:after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html,
body {
    height: 100%;
    width: 100%;
    background: #f2f2f2;
}
body,
input,
button,
select {
    font-family: PingFangSC-Regular, Microsoft YaHei, Roboto, Helvetica, Tahoma,
    Arial, serif;
    font-size: 14px;
    color: #111;
}
input:focus,
select:focus,
button:focus {
    outline: none;
}
a {
    text-decoration: none;
    outline: 0 none;
    color: #111;
}
img {
    border: 0 none;
}
ol,
ul,
li,
table,
tbody {
    margin: 0;
    padding: 0;
    list-style: none;
}

:root {
    --color-white: #fff;
    --color-999: #999;
    --color-green: green;
    --color-red: red;
}

header {
    background: var(--color-white);
    height: 50px;
    line-height: 50px;
    text-align: center;
    font-size: 18px;
    font-weight: 700;
    margin-bottom: 12px;
}

.content {
    background: var(--color-white);
    padding: 10px;
}

.noData {
    color: var(--color-999);
    text-align: center;
}

.list li {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}

.list li:last-child {
    margin-bottom: 0;
}

.list li .edit {
    color: var(--color-green);
    border: none;
    background: none;
    margin-right: 6px;
}

.list li .delete {
    color: var(--color-red);
    border: none;
    background: none;
}

button.add {
    display: block;
    width: 80%;
    padding: 8px 10px;
    margin: 10px auto;
    border-radius: 4px;
    border: none;
    background: var(--color-green);
    color: var(--color-white);
}

.addForm {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: var(--color-white);
}

.addForm h3 {
    position: relative;
    height: 50px;
    line-height: 50px;
    text-align: center;
    margin-bottom: 12px;
}
.addForm h3 span {
    position: absolute;
    right: 10px;
    font-weight: 300;
    font-size: 14px;
    color: var(--color-999);
}
.addForm input {
    display: block;
    width: 92%;
    height: 40px;
    margin: 0 auto 12px;
    padding: 0 8px;
}

.addForm button {
    width: 92%;
    height: 40px;
}

3. 路由文件 html.js

import express from 'express';
import User from '../database/User.js';

const router = express.Router();

router.get('/user-list', async (req, res) => {
  // 这里获取了 user 表的数据
  const users = await User.find();
  res.render(
    'user-list.html',
    {
      title: '首页',
      list: users,
    }
  );
});

export default router;

4. 项目 app.js

import express from 'express';
import {urlencoded, json, static as staticMiddleware} from "express";
import ejs from 'ejs';
import {fileURLToPath} from 'url';
import {dirname, join} from 'path';
import cookieParser from 'cookie-parser';
import { connectDB } from './database/db.js';
import htmlRouter from './router/html.js';
import userRouter from './router/api/user.js';


const app = express();
const port = 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

app.set('views', join(__dirname, '/template'));
app.set('view engine', 'html');
app.engine('html', ejs.__express);

app.use(staticMiddleware(join(__dirname, 'public')));
app.use(urlencoded({ extended: true }));
app.use(json());
app.use(cookieParser('zhang1992'));

// 页面路由
app.use('/', htmlRouter);

// 用户接口路由
app.use('/user', userRouter);


// 错误处理中间件
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    code: '-1',
    msg: err.message,
  });
})

// 链接数据库并启动项目
connectDB().then(() => {
  app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
  });
});
最终展示效果

image.png