SQLite 关系型数据存储:一对多与多对多关系完整指南
概述
SQLite 作为轻量级关系型数据库,在 Node.js CLI 工具开发中广泛应用。本文将深入探讨如何使用 SQLite 存储复杂的关系型数据,特别是一对多和多对多关系的实现与优化。
为什么选择关系型存储?
当数据结构变得复杂时,JSON 文件存储会出现以下问题:
- 数据冗余严重
- 更新操作复杂
- 查询效率低下
- 数据一致性难以保证
关系型数据库通过外键约束、索引优化和规范化设计,能够有效解决这些问题。
一对多关系详解
概念与原理
一对多关系(One-to-Many)是最常见的关系类型,表示一个实体可以与多个其他实体相关联,而这些被关联的实体只能属于一个主实体。
实际应用场景
| 主实体 | 从实体 | 业务场景 |
|---|---|---|
| 部门 | 员工 | 组织架构管理 |
| 作者 | 文章 | 内容管理系统 |
| 订单 | 商品 | 电商订单系统 |
| 班级 | 学生 | 教育管理系统 |
| 用户 | 订单 | 用户行为跟踪 |
数据库设计
表结构设计
sql
-- 主表:部门表
CREATE TABLE departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 从表:员工表(包含外键)
CREATE TABLE employees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
department_id INTEGER NOT NULL,
position VARCHAR(100),
salary DECIMAL(10,2),
hire_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- 外键约束
FOREIGN KEY (department_id) REFERENCES departments(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- 优化:添加索引
CREATE INDEX idx_employees_department_id ON employees(department_id);
CREATE INDEX idx_employees_email ON employees(email);关键设计要点
- 外键位置:外键总是放在"多"的一方
- 级联操作:
ON DELETE CASCADE确保数据完整性 - 索引优化:为外键字段添加索引提升查询性能
- 约束设计:合理使用 NOT NULL、UNIQUE 等约束
Node.js 实现
环境准备
bash
npm install sqlite3 better-sqlite3
# 或者使用更现代的 better-sqlite3
npm install better-sqlite3完整示例代码
javascript
// database.js - 数据库连接管理
const Database = require("better-sqlite3")
const path = require("path")
class DatabaseManager {
constructor(dbPath = "company.db") {
this.db = new Database(dbPath)
this.initializeTables()
}
initializeTables() {
// 启用外键约束
this.db.pragma("foreign_keys = ON")
// 创建部门表
this.db.exec(`
CREATE TABLE IF NOT EXISTS departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
// 创建员工表
this.db.exec(`
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
department_id INTEGER NOT NULL,
position VARCHAR(100),
salary DECIMAL(10,2),
hire_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (department_id) REFERENCES departments(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
`)
// 创建索引
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_employees_department_id ON employees(department_id);
CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(email);
`)
}
close() {
this.db.close()
}
}
module.exports = DatabaseManager数据操作类
javascript
// departmentService.js - 部门服务
class DepartmentService {
constructor(database) {
this.db = database
}
// 创建部门
createDepartment(departmentData) {
const { name, description } = departmentData
const stmt = this.db.prepare(`
INSERT INTO departments (name, description)
VALUES (?, ?)
`)
try {
const result = stmt.run(name, description)
return this.getDepartmentById(result.lastInsertRowid)
} catch (error) {
if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
throw new Error(`部门 "${name}" 已存在`)
}
throw error
}
}
// 获取部门及其员工
getDepartmentWithEmployees(departmentId) {
const stmt = this.db.prepare(`
SELECT
d.id as department_id,
d.name as department_name,
d.description,
e.id as employee_id,
e.name as employee_name,
e.email,
e.position,
e.salary,
e.hire_date
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
WHERE d.id = ?
ORDER BY e.name
`)
const rows = stmt.all(departmentId)
if (rows.length === 0) {
return null
}
// 重构数据结构
const department = {
id: rows[0].department_id,
name: rows[0].department_name,
description: rows[0].description,
employees: []
}
rows.forEach((row) => {
if (row.employee_id) {
department.employees.push({
id: row.employee_id,
name: row.employee_name,
email: row.email,
position: row.position,
salary: row.salary,
hire_date: row.hire_date
})
}
})
return department
}
// 获取部门统计信息
getDepartmentStats() {
const stmt = this.db.prepare(`
SELECT
d.id,
d.name,
d.description,
COUNT(e.id) as employee_count,
AVG(e.salary) as average_salary,
MAX(e.salary) as max_salary,
MIN(e.salary) as min_salary
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.id, d.name, d.description
ORDER BY employee_count DESC
`)
return stmt.all()
}
// 更新部门
updateDepartment(departmentId, updateData) {
const { name, description } = updateData
const stmt = this.db.prepare(`
UPDATE departments
SET name = COALESCE(?, name),
description = COALESCE(?, description),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`)
const result = stmt.run(name, description, departmentId)
if (result.changes === 0) {
throw new Error(`部门 ID ${departmentId} 不存在`)
}
return this.getDepartmentById(departmentId)
}
// 删除部门(级联删除员工)
deleteDepartment(departmentId) {
const stmt = this.db.prepare("DELETE FROM departments WHERE id = ?")
const result = stmt.run(departmentId)
if (result.changes === 0) {
throw new Error(`部门 ID ${departmentId} 不存在`)
}
return { deleted: true, departmentId }
}
getDepartmentById(departmentId) {
const stmt = this.db.prepare("SELECT * FROM departments WHERE id = ?")
return stmt.get(departmentId)
}
}
module.exports = DepartmentService员工服务类
javascript
// employeeService.js - 员工服务
class EmployeeService {
constructor(database) {
this.db = database
}
// 创建员工
createEmployee(employeeData) {
const { name, email, department_id, position, salary, hire_date } = employeeData
// 验证部门存在性
const deptStmt = this.db.prepare("SELECT id FROM departments WHERE id = ?")
const department = deptStmt.get(department_id)
if (!department) {
throw new Error(`部门 ID ${department_id} 不存在`)
}
const stmt = this.db.prepare(`
INSERT INTO employees (name, email, department_id, position, salary, hire_date)
VALUES (?, ?, ?, ?, ?, ?)
`)
try {
const result = stmt.run(
name,
email,
department_id,
position,
salary,
hire_date
)
return this.getEmployeeById(result.lastInsertRowid)
} catch (error) {
if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
throw new Error(`邮箱 "${email}" 已被使用`)
}
throw error
}
}
// 获取员工详情(包含部门信息)
getEmployeeWithDepartment(employeeId) {
const stmt = this.db.prepare(`
SELECT
e.*,
d.name as department_name,
d.description as department_description
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.id = ?
`)
return stmt.get(employeeId)
}
// 更新员工部门
transferEmployee(employeeId, newDepartmentId) {
// 验证新部门存在性
const deptStmt = this.db.prepare("SELECT id FROM departments WHERE id = ?")
const department = deptStmt.get(newDepartmentId)
if (!department) {
throw new Error(`部门 ID ${newDepartmentId} 不存在`)
}
const stmt = this.db.prepare(`
UPDATE employees
SET department_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`)
const result = stmt.run(newDepartmentId, employeeId)
if (result.changes === 0) {
throw new Error(`员工 ID ${employeeId} 不存在`)
}
return this.getEmployeeById(employeeId)
}
// 分页查询员工
getEmployeesPaged(page = 1, pageSize = 10, departmentId = null) {
const offset = (page - 1) * pageSize
let whereClause = ""
let params = []
if (departmentId) {
whereClause = "WHERE e.department_id = ?"
params.push(departmentId)
}
const countStmt = this.db.prepare(`
SELECT COUNT(*) as total
FROM employees e
${whereClause}
`)
const dataStmt = this.db.prepare(`
SELECT
e.*,
d.name as department_name
FROM employees e
JOIN departments d ON e.department_id = d.id
${whereClause}
ORDER BY e.name
LIMIT ? OFFSET ?
`)
const total = countStmt.get(...params)?.total || 0
params.push(pageSize, offset)
const data = dataStmt.all(...params)
return {
data,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
}
}
getEmployeeById(employeeId) {
const stmt = this.db.prepare("SELECT * FROM employees WHERE id = ?")
return stmt.get(employeeId)
}
}
module.exports = EmployeeService使用示例
javascript
// app.js - 应用主文件
const DatabaseManager = require("./database")
const DepartmentService = require("./departmentService")
const EmployeeService = require("./employeeService")
async function main() {
// 初始化数据库
const dbManager = new DatabaseManager("company.db")
const departmentService = new DepartmentService(dbManager.db)
const employeeService = new EmployeeService(dbManager.db)
try {
// 1. 创建部门
console.log("=== 创建部门 ===")
const techDept = departmentService.createDepartment({
name: "技术部",
description: "负责产品研发和技术创新"
})
const salesDept = departmentService.createDepartment({
name: "销售部",
description: "负责产品销售和客户关系维护"
})
console.log("技术部创建成功:", techDept)
console.log("销售部创建成功:", salesDept)
// 2. 创建员工
console.log("\n=== 创建员工 ===")
const employee1 = employeeService.createEmployee({
name: "张三",
email: "zhangsan@company.com",
department_id: techDept.id,
position: "高级工程师",
salary: 15000,
hire_date: "2023-01-15"
})
const employee2 = employeeService.createEmployee({
name: "李四",
email: "lisi@company.com",
department_id: techDept.id,
position: "前端工程师",
salary: 12000,
hire_date: "2023-03-20"
})
const employee3 = employeeService.createEmployee({
name: "王五",
email: "wangwu@company.com",
department_id: salesDept.id,
position: "销售经理",
salary: 10000,
hire_date: "2022-11-10"
})
console.log("员工创建成功:", employee1, employee2, employee3)
// 3. 查询部门及其员工
console.log("\n=== 查询技术部及其员工 ===")
const techDeptWithEmployees = departmentService.getDepartmentWithEmployees(
techDept.id
)
console.log(JSON.stringify(techDeptWithEmployees, null, 2))
// 4. 部门统计
console.log("\n=== 部门统计 ===")
const stats = departmentService.getDepartmentStats()
console.log("部门统计:", stats)
// 5. 分页查询员工
console.log("\n=== 员工分页查询 ===")
const employeesPage = employeeService.getEmployeesPaged(1, 2)
console.log("员工分页数据:", employeesPage)
// 6. 员工调岗
console.log("\n=== 员工调岗 ===")
const transferredEmployee = employeeService.transferEmployee(
employee2.id,
salesDept.id
)
console.log("员工调岗成功:", transferredEmployee)
// 7. 验证调岗结果
console.log("\n=== 验证调岗结果 ===")
const updatedSalesDept = departmentService.getDepartmentWithEmployees(
salesDept.id
)
console.log("销售部最新员工列表:", updatedSalesDept)
} catch (error) {
console.error("操作失败:", error.message)
} finally {
dbManager.close()
}
}
// 运行应用
if (require.main === module) {
main()
}
module.exports = { DatabaseManager, DepartmentService, EmployeeService }性能优化建议
-
索引策略
- 为外键字段创建索引
- 为常用查询字段创建复合索引
- 避免过度索引影响写入性能
-
查询优化
- 使用 JOIN 代替子查询
- 合理使用 LIMIT 和 OFFSET
- 避免 SELECT *,只查询需要的字段
-
事务管理
- 批量操作使用事务
- 合理设置事务隔离级别
- 避免长事务
-
数据完整性
- 使用外键约束
- 设置适当的级联操作
- 添加数据验证逻辑
多对多关系详解
概念与原理
多对多关系(Many-to-Many)表示两个实体可以相互关联,且每个实体都可以与多个对方实体相关联。这种关系需要通过中间表(关联表)来实现。
实际应用场景
| 实体 A | 实体 B | 中间表 | 业务场景 |
|---|---|---|---|
| 文章 | 标签 | 文章标签表 | 内容分类系统 |
| 学生 | 课程 | 选课表 | 教育管理系统 |
| 用户 | 角色 | 用户角色表 | 权限管理系统 |
| 产品 | 分类 | 产品分类表 | 电商系统 |
| 医生 | 患者 | 就诊记录表 | 医疗管理系统 |
数据库设计
表结构设计
sql
-- 文章表
CREATE TABLE articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
author VARCHAR(100),
publish_date DATE,
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 标签表
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description TEXT,
color VARCHAR(7) DEFAULT '#007bff',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 文章标签关联表(中间表)
CREATE TABLE article_tags (
article_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- 优化索引
CREATE INDEX idx_article_tags_article_id ON article_tags(article_id);
CREATE INDEX idx_article_tags_tag_id ON article_tags(tag_id);
CREATE INDEX idx_articles_status ON articles(status);
CREATE INDEX idx_articles_publish_date ON articles(publish_date);设计要点
- 复合主键:中间表使用复合主键确保唯一性
- 双向外键:两个外键分别指向两个主表
- 级联删除:删除主表记录时自动删除关联记录
- 时间戳:记录关联关系的创建时间
Node.js 实现
文章服务类
javascript
// articleService.js - 文章服务
class ArticleService {
constructor(database) {
this.db = database
}
// 创建文章(带标签)
createArticle(articleData) {
const { title, content, author, publish_date, status, tags } = articleData
// 开始事务
const transaction = this.db.transaction(() => {
// 创建文章
const articleStmt = this.db.prepare(`
INSERT INTO articles (title, content, author, publish_date, status)
VALUES (?, ?, ?, ?, ?)
`)
const articleResult = articleStmt.run(
title,
content,
author,
publish_date,
status
)
const articleId = articleResult.lastInsertRowid
// 处理标签
if (tags && tags.length > 0) {
this.addTagsToArticle(articleId, tags)
}
return this.getArticleById(articleId)
})
return transaction()
}
// 为文章添加标签
addTagsToArticle(articleId, tagIds) {
// 验证标签存在性
const tagStmt = this.db.prepare(`
SELECT id FROM tags WHERE id IN (${tagIds.map(() => "?").join(",")})
`)
const existingTags = tagStmt.all(...tagIds)
if (existingTags.length !== tagIds.length) {
throw new Error("部分标签不存在")
}
// 批量插入关联关系
const insertStmt = this.db.prepare(`
INSERT OR IGNORE INTO article_tags (article_id, tag_id)
VALUES (?, ?)
`)
const insertMany = this.db.transaction((articleId, tagIds) => {
for (const tagId of tagIds) {
insertStmt.run(articleId, tagId)
}
})
insertMany(articleId, tagIds)
}
// 获取文章详情(含标签)
getArticleById(articleId) {
// 获取文章基本信息
const articleStmt = this.db.prepare("SELECT * FROM articles WHERE id = ?")
const article = articleStmt.get(articleId)
if (!article) {
return null
}
// 获取关联标签
const tagsStmt = this.db.prepare(`
SELECT t.*
FROM tags t
JOIN article_tags at ON t.id = at.tag_id
WHERE at.article_id = ?
ORDER BY t.name
`)
const tags = tagsStmt.all(articleId)
return {
...article,
tags
}
}
// 根据标签查询文章
getArticlesByTag(tagId, page = 1, pageSize = 10) {
const offset = (page - 1) * pageSize
// 获取总数
const countStmt = this.db.prepare(`
SELECT COUNT(DISTINCT a.id) as total
FROM articles a
JOIN article_tags at ON a.id = at.article_id
WHERE at.tag_id = ? AND a.status = 'published'
`)
// 获取分页数据
const dataStmt = this.db.prepare(`
SELECT DISTINCT a.*,
GROUP_CONCAT(t.name, ', ') as tag_names
FROM articles a
JOIN article_tags at ON a.id = at.article_id
JOIN tags t ON at.tag_id = t.id
WHERE at.tag_id = ? AND a.status = 'published'
GROUP BY a.id
ORDER BY a.publish_date DESC
LIMIT ? OFFSET ?
`)
const total = countStmt.get(tagId)?.total || 0
const data = dataStmt.all(tagId, pageSize, offset)
return {
data,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
}
}
// 获取热门标签
getPopularTags(limit = 10) {
const stmt = this.db.prepare(`
SELECT
t.*,
COUNT(at.article_id) as article_count
FROM tags t
JOIN article_tags at ON t.id = at.tag_id
JOIN articles a ON at.article_id = a.id
WHERE a.status = 'published'
GROUP BY t.id, t.name, t.description, t.color
ORDER BY article_count DESC, t.name
LIMIT ?
`)
return stmt.all(limit)
}
// 更新文章标签
updateArticleTags(articleId, newTagIds) {
const transaction = this.db.transaction(() => {
// 删除现有标签关联
const deleteStmt = this.db.prepare(
"DELETE FROM article_tags WHERE article_id = ?"
)
deleteStmt.run(articleId)
// 添加新标签
if (newTagIds && newTagIds.length > 0) {
this.addTagsToArticle(articleId, newTagIds)
}
return this.getArticleById(articleId)
})
return transaction()
}
// 删除文章
deleteArticle(articleId) {
const stmt = this.db.prepare("DELETE FROM articles WHERE id = ?")
const result = stmt.run(articleId)
if (result.changes === 0) {
throw new Error(`文章 ID ${articleId} 不存在`)
}
return { deleted: true, articleId }
}
}
module.exports = ArticleService标签服务类
javascript
// tagService.js - 标签服务
class TagService {
constructor(database) {
this.db = database
}
// 创建标签
createTag(tagData) {
const { name, description, color } = tagData
const stmt = this.db.prepare(`
INSERT INTO tags (name, description, color)
VALUES (?, ?, ?)
`)
try {
const result = stmt.run(name, description, color)
return this.getTagById(result.lastInsertRowid)
} catch (error) {
if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
throw new Error(`标签 "${name}" 已存在`)
}
throw error
}
}
// 批量创建标签
createTagsBatch(tagsData) {
const transaction = this.db.transaction(() => {
const stmt = this.db.prepare(`
INSERT OR IGNORE INTO tags (name, description, color)
VALUES (?, ?, ?)
`)
const results = []
for (const tagData of tagsData) {
const result = stmt.run(tagData.name, tagData.description, tagData.color)
if (result.changes > 0) {
results.push(this.getTagById(result.lastInsertRowid))
}
}
return results
})
return transaction()
}
// 获取标签及其文章
getTagWithArticles(tagId) {
// 获取标签信息
const tagStmt = this.db.prepare("SELECT * FROM tags WHERE id = ?")
const tag = tagStmt.get(tagId)
if (!tag) {
return null
}
// 获取关联文章
const articlesStmt = this.db.prepare(`
SELECT a.*
FROM articles a
JOIN article_tags at ON a.id = at.article_id
WHERE at.tag_id = ? AND a.status = 'published'
ORDER BY a.publish_date DESC
`)
const articles = articlesStmt.all(tagId)
return {
...tag,
articles,
article_count: articles.length
}
}
// 获取所有标签(含统计)
getAllTagsWithStats() {
const stmt = this.db.prepare(`
SELECT
t.*,
COUNT(DISTINCT a.id) as article_count,
MAX(a.publish_date) as latest_article_date
FROM tags t
LEFT JOIN article_tags at ON t.id = at.tag_id
LEFT JOIN articles a ON at.article_id = a.id AND a.status = 'published'
GROUP BY t.id, t.name, t.description, t.color
ORDER BY article_count DESC, t.name
`)
return stmt.all()
}
// 搜索标签
searchTags(query, limit = 10) {
const stmt = this.db.prepare(`
SELECT
t.*,
COUNT(at.article_id) as article_count
FROM tags t
LEFT JOIN article_tags at ON t.id = at.tag_id
LEFT JOIN articles a ON at.article_id = a.id AND a.status = 'published'
WHERE t.name LIKE ? OR t.description LIKE ?
GROUP BY t.id, t.name, t.description, t.color
ORDER BY article_count DESC, t.name
LIMIT ?
`)
const searchPattern = `%${query}%`
return stmt.all(searchPattern, searchPattern, limit)
}
// 更新标签
updateTag(tagId, updateData) {
const { name, description, color } = updateData
const stmt = this.db.prepare(`
UPDATE tags
SET name = COALESCE(?, name),
description = COALESCE(?, description),
color = COALESCE(?, color)
WHERE id = ?
`)
const result = stmt.run(name, description, color, tagId)
if (result.changes === 0) {
throw new Error(`标签 ID ${tagId} 不存在`)
}
return this.getTagById(tagId)
}
// 删除标签
deleteTag(tagId) {
const stmt = this.db.prepare("DELETE FROM tags WHERE id = ?")
const result = stmt.run(tagId)
if (result.changes === 0) {
throw new Error(`标签 ID ${tagId} 不存在`)
}
return { deleted: true, tagId }
}
getTagById(tagId) {
const stmt = this.db.prepare("SELECT * FROM tags WHERE id = ?")
return stmt.get(tagId)
}
}
module.exports = TagService完整使用示例
javascript
// blogApp.js - 博客应用示例
const Database = require("better-sqlite3")
const ArticleService = require("./articleService")
const TagService = require("./tagService")
class BlogManager {
constructor(dbPath = "blog.db") {
this.db = new Database(dbPath)
this.db.pragma("foreign_keys = ON")
this.initializeTables()
this.articleService = new ArticleService(this.db)
this.tagService = new TagService(this.db)
}
initializeTables() {
// 创建文章表
this.db.exec(`
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT,
author VARCHAR(100),
publish_date DATE,
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
// 创建标签表
this.db.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description TEXT,
color VARCHAR(7) DEFAULT '#007bff',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
// 创建关联表
this.db.exec(`
CREATE TABLE IF NOT EXISTS article_tags (
article_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
`)
// 创建索引
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_article_tags_article_id ON article_tags(article_id);
CREATE INDEX IF NOT EXISTS idx_article_tags_tag_id ON article_tags(tag_id);
CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status);
CREATE INDEX IF NOT EXISTS idx_articles_publish_date ON articles(publish_date);
`)
}
close() {
this.db.close()
}
}
// 演示应用
async function main() {
const blogManager = new BlogManager("blog.db")
try {
console.log("=== 创建标签 ===")
// 批量创建标签
const tags = blogManager.tagService.createTagsBatch([
{ name: "JavaScript", description: "JavaScript 相关文章", color: "#f7df1e" },
{ name: "Node.js", description: "Node.js 技术文章", color: "#339933" },
{ name: "数据库", description: "数据库相关技术", color: "#336791" },
{ name: "前端", description: "前端开发技术", color: "#61dafb" },
{ name: "后端", description: "后端开发技术", color: "#764abc" },
{ name: "教程", description: "技术教程和指南", color: "#ff6b6b" }
])
console.log("标签创建成功,数量:", tags.length)
console.log("\n=== 创建文章 ===")
// 创建文章并关联标签
const article1 = blogManager.articleService.createArticle({
title: "Node.js SQLite 完整指南",
content: "SQLite 是一个轻量级的关系型数据库,非常适合 Node.js 应用...",
author: "张三",
publish_date: "2024-01-15",
status: "published",
tags: [1, 2, 3, 6] // JavaScript, Node.js, 数据库, 教程
})
const article2 = blogManager.articleService.createArticle({
title: "前端性能优化最佳实践",
content: "前端性能优化是提升用户体验的关键...",
author: "李四",
publish_date: "2024-01-20",
status: "published",
tags: [1, 4, 6] // JavaScript, 前端, 教程
})
console.log("文章创建成功:", article1.title, article2.title)
console.log("\n=== 查询文章详情 ===")
// 查询文章详情
const detailedArticle = blogManager.articleService.getArticleById(1)
console.log("文章详情:", JSON.stringify(detailedArticle, null, 2))
console.log("\n=== 根据标签查询文章 ===")
// 根据标签查询文章
const jsArticles = blogManager.articleService.getArticlesByTag(1, 1, 10)
console.log("JavaScript 标签的文章:", jsArticles)
console.log("\n=== 获取热门标签 ===")
// 获取热门标签
const popularTags = blogManager.articleService.getPopularTags(5)
console.log("热门标签:", popularTags)
console.log("\n=== 更新文章标签 ===")
// 更新文章标签
const updatedArticle = blogManager.articleService.updateArticleTags(1, [1, 2, 6])
console.log(
"文章标签更新成功:",
updatedArticle.tags.map((tag) => tag.name)
)
console.log("\n=== 搜索标签 ===")
// 搜索标签
const searchResults = blogManager.tagService.searchTags("Java")
console.log("标签搜索结果:", searchResults)
console.log("\n=== 获取所有标签统计 ===")
// 获取所有标签统计
const allTagsStats = blogManager.tagService.getAllTagsWithStats()
console.log("标签统计:", allTagsStats)
} catch (error) {
console.error("操作失败:", error.message)
} finally {
blogManager.close()
}
}
// 运行演示
if (require.main === module) {
main()
}
module.exports = BlogManager多对多关系最佳实践
-
中间表设计
- 使用复合主键确保唯一性
- 添加创建时间等元数据
- 考虑添加额外的关联属性
-
查询优化
- 使用 JOIN 进行关联查询
- 合理使用 GROUP BY 和聚合函数
- 为关联字段创建索引
-
事务管理
- 批量操作使用事务
- 确保数据一致性
- 处理并发操作
-
性能考虑
- 避免 N+1 查询问题
- 使用分页处理大量数据
- 定期分析和优化查询
高级主题
复杂关系模型
多层级关系
sql
-- 分类层级结构(支持无限层级)
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
parent_id INTEGER,
level INTEGER DEFAULT 0,
path VARCHAR(500),
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE CASCADE
);
-- 创建索引
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
CREATE INDEX idx_categories_path ON categories(path);多对多关系的扩展
sql
-- 用户-角色-权限三级关系
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT
);
CREATE TABLE permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
resource VARCHAR(100),
action VARCHAR(50)
);
-- 用户-角色关联
CREATE TABLE user_roles (
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
-- 角色-权限关联
CREATE TABLE role_permissions (
role_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);性能优化策略
1. 索引优化
sql
-- 复合索引
CREATE INDEX idx_articles_author_status_date ON articles(author, status, publish_date);
-- 部分索引(只对满足条件的数据建立索引)
CREATE INDEX idx_articles_published ON articles(publish_date)
WHERE status = 'published';
-- 覆盖索引(包含查询所需的所有字段)
CREATE INDEX idx_employees_covering ON employees(department_id, name, email, position);2. 查询优化
javascript
// 使用子查询优化
const optimizedQuery = `
SELECT a.*, u.username,
(SELECT GROUP_CONCAT(t.name, ', ')
FROM tags t
JOIN article_tags at ON t.id = at.tag_id
WHERE at.article_id = a.id) as tag_names
FROM articles a
JOIN users u ON a.author_id = u.id
WHERE a.status = 'published'
ORDER BY a.publish_date DESC
LIMIT ? OFFSET ?
`
// 使用窗口函数(SQLite 3.25+)
const windowFunctionQuery = `
SELECT
a.*,
ROW_NUMBER() OVER (PARTITION BY a.author_id ORDER BY a.publish_date DESC) as author_rank,
COUNT(*) OVER (PARTITION BY a.author_id) as author_article_count
FROM articles a
WHERE a.status = 'published'
`3. 数据分区
javascript
// 按时间分区存储
const partitionByMonth = (tableName, dateColumn) => {
return `
CREATE TABLE IF NOT EXISTS ${tableName}_${new Date().getFullYear()}_${(
new Date().getMonth() + 1
)
.toString()
.padStart(2, "0")} (
LIKE ${tableName} INCLUDING ALL
);
CREATE INDEX IF NOT EXISTS idx_${tableName}_${new Date().getFullYear()}_${(
new Date().getMonth() + 1
)
.toString()
.padStart(2, "0")}_${dateColumn}
ON ${tableName}_${new Date().getFullYear()}_${(new Date().getMonth() + 1)
.toString()
.padStart(2, "0")}(${dateColumn});
`
}错误处理与调试
1. 错误处理策略
javascript
class DatabaseError extends Error {
constructor(message, code, sql) {
super(message)
this.name = "DatabaseError"
this.code = code
this.sql = sql
}
}
const handleDatabaseError = (error, operation) => {
console.error(`数据库操作失败: ${operation}`, {
message: error.message,
code: error.code,
sql: error.sql,
stack: error.stack
})
// 根据错误类型返回不同的错误信息
switch (error.code) {
case "SQLITE_CONSTRAINT_UNIQUE":
throw new DatabaseError("数据已存在,无法重复创建", error.code, error.sql)
case "SQLITE_CONSTRAINT_FOREIGNKEY":
throw new DatabaseError("关联数据不存在,操作失败", error.code, error.sql)
case "SQLITE_BUSY":
throw new DatabaseError("数据库繁忙,请稍后重试", error.code, error.sql)
default:
throw new DatabaseError(
`数据库操作失败: ${error.message}`,
error.code,
error.sql
)
}
}2. 调试技巧
javascript
// 启用查询日志
const enableQueryLogging = (db) => {
db.on("trace", (sql) => {
console.log("SQL执行:", sql)
})
db.on("profile", (sql, time) => {
console.log(`SQL执行耗时: ${time}ms - ${sql}`)
})
}
// 性能分析
const analyzePerformance = (db) => {
// 分析查询计划
const explainQuery = (sql) => {
const explainStmt = db.prepare(`EXPLAIN QUERY PLAN ${sql}`)
return explainStmt.all()
}
// 获取数据库统计信息
const getDatabaseStats = () => {
return {
tableCount: db
.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table'")
.get().count,
indexCount: db
.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='index'")
.get().count,
totalSize: db
.prepare(
"SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"
)
.get().size
}
}
return { explainQuery, getDatabaseStats }
}数据迁移与备份
1. 数据迁移脚本
javascript
// migration.js - 数据迁移工具
const migrateData = async (sourceDb, targetDb) => {
try {
// 获取所有表结构
const tables = sourceDb
.prepare(
`
SELECT name, sql
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`
)
.all()
// 在目标数据库中创建表
for (const table of tables) {
targetDb.exec(table.sql)
}
// 复制数据
for (const table of tables) {
const tableName = table.name
console.log(`迁移表: ${tableName}`)
// 获取源数据
const sourceData = sourceDb.prepare(`SELECT * FROM ${tableName}`).all()
if (sourceData.length > 0) {
// 构建插入语句
const columns = Object.keys(sourceData[0])
const placeholders = columns.map(() => "?").join(",")
const insertStmt = targetDb.prepare(`
INSERT INTO ${tableName} (${columns.join(",")})
VALUES (${placeholders})
`)
// 批量插入
const insertMany = targetDb.transaction((data) => {
for (const row of data) {
const values = columns.map((col) => row[col])
insertStmt.run(...values)
}
})
insertMany(sourceData)
console.log(`迁移完成: ${tableName} - ${sourceData.length} 条记录`)
}
}
console.log("数据迁移完成")
} catch (error) {
console.error("数据迁移失败:", error)
throw error
}
}2. 备份策略
javascript
// backup.js - 备份工具
const fs = require("fs")
const path = require("path")
class BackupManager {
constructor(dbPath, backupDir = "./backups") {
this.dbPath = dbPath
this.backupDir = backupDir
this.ensureBackupDir()
}
ensureBackupDir() {
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true })
}
}
// 创建备份
createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const backupFile = path.join(this.backupDir, `backup_${timestamp}.db`)
try {
// 使用 SQLite 的备份 API
const sourceDb = new Database(this.dbPath)
const backupDb = new Database(backupFile)
// 执行备份
sourceDb
.backup(backupFile)
.then(() => {
console.log(`备份创建成功: ${backupFile}`)
sourceDb.close()
backupDb.close()
// 清理旧备份(保留最近7天)
this.cleanupOldBackups(7)
})
.catch((error) => {
console.error("备份创建失败:", error)
sourceDb.close()
backupDb.close()
})
return backupFile
} catch (error) {
console.error("备份过程失败:", error)
throw error
}
}
// 清理旧备份
cleanupOldBackups(daysToKeep) {
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep)
const files = fs.readdirSync(this.backupDir)
files.forEach((file) => {
const filePath = path.join(this.backupDir, file)
const stats = fs.statSync(filePath)
if (stats.mtime < cutoffDate && file.startsWith("backup_")) {
fs.unlinkSync(filePath)
console.log(`删除旧备份: ${file}`)
}
})
}
// 恢复备份
restoreBackup(backupFile) {
try {
// 验证备份文件存在
if (!fs.existsSync(backupFile)) {
throw new Error(`备份文件不存在: ${backupFile}`)
}
// 创建当前数据库的临时备份
const tempBackup = this.dbPath + ".temp"
if (fs.existsSync(this.dbPath)) {
fs.copyFileSync(this.dbPath, tempBackup)
}
try {
// 恢复备份
fs.copyFileSync(backupFile, this.dbPath)
console.log(`数据库恢复成功: ${backupFile}`)
// 删除临时备份
if (fs.existsSync(tempBackup)) {
fs.unlinkSync(tempBackup)
}
} catch (error) {
// 恢复失败时回滚
if (fs.existsSync(tempBackup)) {
fs.copyFileSync(tempBackup, this.dbPath)
fs.unlinkSync(tempBackup)
}
throw error
}
} catch (error) {
console.error("数据库恢复失败:", error)
throw error
}
}
}
module.exports = BackupManager常见问题与解决方案
1. 外键约束失败
问题:插入数据时外键约束失败
解决方案:
javascript
// 在插入前验证关联数据存在性
const validateForeignKeys = (db, table, foreignKeys) => {
for (const [column, referencedTable, referencedColumn] of foreignKeys) {
const checkStmt = db.prepare(`
SELECT 1 FROM ${referencedTable}
WHERE ${referencedColumn} = ?
`)
if (!checkStmt.get(value)) {
throw new Error(`${referencedTable} 中不存在 ${referencedColumn} = ${value}`)
}
}
}2. 性能问题
问题:查询大量关联数据时性能低下
解决方案:
javascript
// 使用延迟加载策略
const getArticlesWithTagsLazy = (articleIds) => {
// 第一步:获取文章基本信息
const articlesStmt = db.prepare(`
SELECT * FROM articles WHERE id IN (${articleIds.map(() => "?").join(",")})
`)
const articles = articlesStmt.all(...articleIds)
// 第二步:批量获取标签信息
const tagsStmt = db.prepare(`
SELECT at.article_id, t.*
FROM article_tags at
JOIN tags t ON at.tag_id = t.id
WHERE at.article_id IN (${articleIds.map(() => "?").join(",")})
`)
const tags = tagsStmt.all(...articleIds)
// 第三步:组合数据
const tagsMap = {}
tags.forEach((tag) => {
if (!tagsMap[tag.article_id]) {
tagsMap[tag.article_id] = []
}
tagsMap[tag.article_id].push(tag)
})
return articles.map((article) => ({
...article,
tags: tagsMap[article.id] || []
}))
}3. 并发问题
问题:多用户同时操作导致数据不一致
解决方案:
javascript
// 使用乐观锁
const updateArticleWithVersion = (articleId, updateData, currentVersion) => {
const transaction = db.transaction(() => {
// 检查版本号
const checkStmt = db.prepare("SELECT version FROM articles WHERE id = ?")
const { version } = checkStmt.get(articleId)
if (version !== currentVersion) {
throw new Error("数据已被其他用户修改,请刷新后重试")
}
// 更新数据并增加版本号
const updateStmt = db.prepare(`
UPDATE articles
SET title = ?, content = ?, version = version + 1, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND version = ?
`)
const result = updateStmt.run(
updateData.title,
updateData.content,
articleId,
currentVersion
)
if (result.changes === 0) {
throw new Error("更新失败,数据可能已被其他用户修改")
}
return { success: true, newVersion: currentVersion + 1 }
})
return transaction()
}4. 内存使用问题
问题:处理大量数据时内存溢出
解决方案:
javascript
// 使用流式处理
const processLargeDataset = (db, table, batchSize = 1000, processor) => {
return new Promise((resolve, reject) => {
let offset = 0
let hasMore = true
const processBatch = () => {
try {
const stmt = db.prepare(`
SELECT * FROM ${table}
ORDER BY id
LIMIT ? OFFSET ?
`)
const rows = stmt.all(batchSize, offset)
if (rows.length === 0) {
hasMore = false
resolve()
return
}
// 处理当前批次
processor(rows)
offset += batchSize
// 继续处理下一批次
if (hasMore) {
setImmediate(processBatch)
}
} catch (error) {
reject(error)
}
}
processBatch()
})
}总结与最佳实践
核心要点回顾
-
关系设计原则
- 一对多关系:外键放在"多"的一方
- 多对多关系:使用中间表,复合主键确保唯一性
- 合理使用级联操作维护数据完整性
-
性能优化策略
- 为外键和常用查询字段创建索引
- 使用事务处理批量操作
- 避免 N+1 查询问题
- 合理使用分页和延迟加载
-
数据完整性保障
- 使用外键约束
- 添加适当的验证逻辑
- 实现错误处理机制
- 定期备份数据
最佳实践清单
✅ 应该做的
- 为所有外键字段创建索引
- 使用事务处理相关操作
- 添加数据验证和错误处理
- 定期备份数据库
- 使用参数化查询防止 SQL 注入
- 合理命名表和字段
- 添加创建时间和更新时间字段
- 使用适当的数据类型
❌ 不应该做的
- 在循环中执行单个查询
- 忽略外键约束的重要性
- 使用过大的事务
- 忽略索引对性能的影响
- 在应用层处理数据库约束
- 忽略数据库备份
- 使用 SELECT *查询所有字段
进一步学习资源
- SQLite 官方文档:https://www.sqlite.org/docs.html
- better-sqlite3 文档:https://github.com/JoshuaWise/better-sqlite3
- 数据库设计模式:https://www.databasepatterns.com/
- SQL 性能优化:https://use-the-index-luke.com/
通过本文的学习,你应该能够:
- 正确设计一对多和多对多关系的数据库结构
- 使用 Node.js 实现完整的数据操作逻辑
- 处理常见的性能问题和错误情况
- 实施有效的数据备份和恢复策略
记住,良好的数据库设计是应用成功的基石。在实际项目中,要根据具体业务需求灵活运用这些知识,并持续优化和改进。