Skip to content

Latest commit

 

History

History
1577 lines (1235 loc) · 37.1 KB

File metadata and controls

1577 lines (1235 loc) · 37.1 KB

Model API 文档

Model 层提供 Schema 验证、自定义方法和生命周期钩子,让你像使用 ORM 一样使用 monSQLize。

特性:Schema 验证 · 自定义方法 · 生命周期钩子 · 自动索引


快速开始

const MonSQLize = require('monsqlize');
const { Model } = MonSQLize;

// 1. 定义 Model
Model.define('users', {
    schema: (dsl) => dsl({
        username: 'string:3-32!',
        email: 'email!',
        password: 'string!',
        age: 'number:0-120'
    }),
    methods: (model) => ({
        instance: {
            checkPassword(password) {
                return this.password === password;
            }
        },
        static: {
            async findByUsername(username) {
                return await model.findOne({ username });
            }
        }
    })
});

// 2. 使用 Model
const msq = new MonSQLize({ ... });
await msq.connect();
const User = msq.model('users');

// 插入
await User.insertOne({
    username: 'test',
    email: 'test@example.com',
    password: 'secret123',
    age: 25
});

// 查询并使用方法
const user = await User.findByUsername('test');
if (user.checkPassword('secret123')) {
    console.log('登录成功');
}

API 参考

Model.define(collectionName, definition)

注册 Model 定义。

参数

  • collectionName - 集合名称
  • definition - Model 定义
    • schema (必需) - Schema 定义
    • enums - 枚举配置
    • methods - 自定义方法
    • hooks - 生命周期钩子
    • indexes - 索引定义
Model.define('users', {
    enums: {
        role: 'admin|user|guest'
    },
    schema: function(dsl) {
        return dsl({
            username: 'string:3-32!',
            email: 'email!',
            password: 'string!',
            role: this.enums.role.default('user')
        });
    },
    methods: (model) => ({
        instance: {
            checkPassword(password) {
                return this.password === password;
            }
        },
        static: {
            async findByUsername(username) {
                return await model.findOne({ username });
            }
        }
    }),
    hooks: (model) => ({
        insert: {
            before: async (ctx, docs) => {
                return { ...docs, createdAt: new Date() };
            }
        }
    }),
    indexes: [
        { key: { username: 1 }, unique: true }
    ]
});

Model.get(collectionName)

获取已注册的 Model 定义。

参数

  • collectionName (string) - 集合名称

返回{ collectionName: string, definition: object } | undefined

返回包含 collectionNamedefinition 的包装对象。如果指定的 Model 未注册,则返回 undefined

// 获取已注册的 Model 定义
const userModel = Model.get('users');

if (userModel) {
    console.log(userModel.collectionName); // 'users'
    console.log(userModel.definition);     // { schema: ..., methods: ..., ... }
}

// 未注册的 Model 返回 undefined
const notFound = Model.get('nonexistent');
console.log(notFound); // undefined

Model.has(collectionName)

检查 Model 是否已注册。

参数

  • collectionName (string) - 集合名称

返回boolean

// 检查 Model 是否已注册
if (Model.has('users')) {
    console.log('users Model 已注册');
}

// 条件注册:避免重复定义
if (!Model.has('users')) {
    Model.define('users', {
        schema: (dsl) => dsl({ username: 'string!' })
    });
}

Model.list()

获取所有已注册的 Model 名称。

无参数

返回string[] - 所有已注册 Model 的集合名称数组

// 获取所有已注册的 Model 名称
const models = Model.list();
console.log(models); // ['users', 'posts', 'comments']

// 遍历所有已注册的 Model
for (const name of Model.list()) {
    const model = Model.get(name);
    console.log(`${name}:`, model.definition);
}

Model.redefine(collectionName, definition)(v1.1.7+)

重新定义已注册的 Model。等效于 undefine() + define() 的组合操作。

参数

  • collectionName (string) - 集合名称
  • definition (object) - 新的 Model 定义

返回:void

如果 Model 不存在,行为等同于 define()

⚠️ 注意:如果新定义校验失败,旧定义会被移除(不会回滚)。主要用于开发模式下的 Model 热重载。

可抛出Error(与 define() 相同的校验逻辑)

// 重新定义已注册的 Model
Model.redefine('users', {
    schema: (dsl) => dsl({
        username: 'string:3-32!',
        email: 'email!',
        avatar: 'string'   // 新增字段
    })
});

// 开发模式下的热重载示例
if (process.env.NODE_ENV === 'development') {
    Model.redefine('users', updatedDefinition);
}

Model.undefine(collectionName)(v1.1.7+)

注销已注册的 Model 定义。幂等操作,对不存在的 Model 不抛错。

参数

  • collectionName (string) - 要注销的集合名称

返回boolean - 成功移除返回 true,不存在返回 false

已实例化的 ModelInstance 不受影响。主要用于开发模式下的 Model 热重载。

// 注销已注册的 Model
const removed = Model.undefine('users');
console.log(removed); // true

// 对不存在的 Model 不抛错
const notFound = Model.undefine('nonexistent');
console.log(notFound); // false

// 注销后可重新定义
Model.undefine('users');
Model.define('users', newDefinition);

msq.model(collectionName)

获取 Model 实例。

const User = msq.model('users');

// 继承所有 collection 方法
const users = await User.find({ status: 'active' });
const user = await User.findOne({ username: 'test' });

// 使用自定义静态方法
const admin = await User.findByUsername('admin');

validate(data, options)

验证数据。

const result = User.validate({
    username: 'test',
    email: 'test@example.com'
});

if (!result.valid) {
    console.error('验证失败:', result.errors);
}

Model 自动加载(v1.0.7+)

功能说明

monSQLize 支持自动扫描指定目录,加载所有 Model 定义文件,无需手动调用 Model.define()

使用方式

简化配置

const msq = new MonSQLize({
    type: 'mongodb',
    databaseName: 'mydb',
    config: { uri: '...' },
    models: './models'  // ← 自动加载
});

await msq.connect();  // 自动扫描 models/*.model.{js,ts,mjs,cjs}

// 直接使用(无需 Model.define)
const User = msq.model('users');

完整配置

const msq = new MonSQLize({
    models: {
        path: './models',               // Model 文件目录
        pattern: '*.model.js',          // 文件名模式
        recursive: true                 // 递归扫描子目录
    }
});

Model 文件格式

// models/user.model.js
module.exports = {
    name: 'users',  // 集合名称(必需)
    
    schema: (dsl) => dsl({
        username: 'string:3-32!',
        email: 'email!'
    }),
    
    methods: (model) => ({
        instance: {
            checkPassword(password) {
                return this.password === password;
            }
        },
        static: {
            async findByUsername(username) {
                return await model.findOne({ username });
            }
        }
    }),
    
    hooks: (model) => ({
        insert: {
            before: async (ctx, doc) => {
                doc.createdAt = new Date();
                return doc;
            }
        }
    }),
    
    indexes: [
        { key: { username: 1 }, unique: true }
    ]
};

目录结构

models/
├── user.model.js
├── post.model.js
├── comment.model.js
└── admin/
    ├── role.model.js
    └── permission.model.js

支持的文件格式

  • .js - CommonJS
  • .mjs - ES Module
  • .cjs - CommonJS(显式)
  • .ts - TypeScript(需要 ts-node)

配置选项

选项 类型 默认值 说明
path string - Model 文件目录(必需)
pattern string *.model.{js,ts,mjs,cjs} 文件名模式(支持 glob)
recursive boolean false 是否递归扫描子目录

错误处理

目录不存在

[Model] Models directory not found: /path/to/models

文件格式错误

[Model] ❌ Failed to load models/invalid.model.js: export is null
[Model] ❌ Failed to load models/no-name.model.js: missing 'name' property

重复注册

[Model] Model 'users' already registered, skipping models/user2.model.js

最佳实践

  1. 统一命名规范: 使用 {name}.model.js 格式
  2. 按功能分组: 使用子目录组织(如 admin/public/
  3. 导出格式: 始终使用 module.exports = { name, ... }
  4. 测试环境: 可以禁用自动加载,手动注册测试 Model

注意事项

  • ⚠️ 文件必须包含 name 属性
  • ⚠️ 重复的 Model 名称只会注册第一个
  • ⚠️ 文件语法错误会导致加载失败(记录日志,不中断)
  • ⚠️ TypeScript 文件需要运行时支持(ts-node 或编译后)

与手动注册对比

方式 优点 缺点
手动注册 精确控制、显式依赖 重复代码、维护成本高
自动加载 自动发现、代码简洁 隐式依赖、加载顺序不确定

推荐: 生产环境使用自动加载,测试环境可选择手动注册。


Schema 验证(v1.0.7 默认启用)

功能说明

monSQLize 集成 schema-dsl 提供数据验证功能。从 v1.0.7 开始,Schema 验证默认启用

基本使用

const { Model } = require('monsqlize');

Model.define('users', {
    schema: (dsl) => dsl({
        username: 'string:3-32!',      // 必需,3-32 字符
        email: 'email!',               // 必需,邮箱格式
        password: 'string:6-!',        // 必需,至少 6 字符
        age: 'number:0-120',           // 可选,0-120 范围
        role: 'string?'                // 可选字符串
    })
});

const User = msq.model('users');

// ✅ v1.0.7: 验证自动生效(无需配置)
await User.insertOne({
    username: 'john',
    email: 'john@example.com',
    password: 'secret123',
    age: 25
});

// ❌ 验证失败
try {
    await User.insertOne({
        username: 'ab',        // 太短
        email: 'invalid',      // 邮箱格式错误
        password: '123'        // 太短
    });
} catch (err) {
    console.error(err.code);    // 'VALIDATION_ERROR'
    console.error(err.message); // 'Schema validation failed: ...'
    console.error(err.errors);  // 详细错误数组
}

Schema 语法

类型 语法 示例
字符串 string 'string!' 必需字符串
字符串范围 string:min-max 'string:3-32!' 3-32 字符
数字 number 'number!' 必需数字
数字范围 number:min-max 'number:0-120' 0-120 范围
邮箱 email 'email!' 邮箱格式
URL url 'url!' URL 格式
数组 array 'array!' 必需数组
对象 object 'object!' 必需对象
布尔 boolean 'boolean!' 必需布尔值
日期 date 'date!' Date 对象
可选 type? 'string?' 可选字符串
必需 type! 'string!' 必需字符串

更多语法见 schema-dsl 文档

验证错误详情

try {
    await User.insertOne({ username: 'ab', email: 'invalid' });
} catch (err) {
    console.log(err.code);     // 'VALIDATION_ERROR'
    console.log(err.message);  // 'Schema validation failed: ...'
    console.log(err.errors);   // 详细错误数组
    /*
    [
        {
            field: 'username',
            type: 'length',
            expected: '3-32',
            actual: 2,
            message: 'username must be 3-32 characters'
        },
        {
            field: 'email',
            type: 'format',
            expected: 'email',
            message: 'email must be a valid email address'
        }
    ]
    */
}

禁用验证

全局禁用(不推荐)

Model.define('users', {
    schema: (dsl) => dsl({ ... }),
    options: { validate: false }  // 全局禁用验证
});

单次操作跳过

// 跳过验证(特殊场景,如数据迁移)
await User.insertOne(doc, { skipValidation: true });

v1.0.6 迁移指南

版本 验证行为 配置方式
v1.0.6 需要显式启用 options: { validate: true }
v1.0.7 默认启用 无需配置(默认生效)
v1.0.7 禁用 需要显式禁用 options: { validate: false }

升级注意事项:

  • ✅ 未定义 schema 的 Model 不受影响
  • ✅ 已显式配置 validate: true 的代码无需修改
  • ⚠️ 如需禁用,需添加 validate: false 配置

性能影响

  • 验证开销: 约 5-10% 插入时间增加
  • 缓存优化: Schema 编译后缓存,重复使用无额外开销
  • 跳过选项: 可通过 skipValidation 跳过(不推荐)

最佳实践

  1. 始终定义 Schema: 保证数据质量
  2. 合理使用可选字段: 避免过度严格
  3. 自定义验证: 使用 hooks 添加复杂验证逻辑
  4. 错误处理: 捕获 VALIDATION_ERROR 返回友好错误

常见问题

Q: 如何验证嵌套对象?

A: 使用 schema-dsl 的嵌套语法:

schema: (dsl) => dsl({
    profile: dsl({
        name: 'string!',
        age: 'number!'
    })
})

Q: 如何自定义验证逻辑?

A: 使用 hooks 添加复杂验证:

hooks: (model) => ({
    insert: {
        before: async (ctx, doc) => {
            if (doc.age < 18) {
                throw new Error('Must be 18+');
            }
            return doc;
        }
    }
})

Q: 性能敏感场景如何优化?

A: 批量插入时可选择跳过验证(风险自负):

await User.insertMany(docs, { skipValidation: true });

配置说明

1. schema - 数据验证

定义字段验证规则。

// 推荐:使用 function 可引用 enums
schema: function(dsl) {
    return dsl({
        username: 'string:3-32!',
        email: 'email!',
        age: 'number:0-120',
        role: this.enums.role.default('user')  // 引用 enums
    });
}

// 或直接使用 object
schema: (dsl) => dsl({
    username: 'string:3-32!',
    email: 'email!'
})

常用规则

  • string! - 必填字符串
  • string:3-32 - 长度 3-32
  • number:0-120 - 数字范围
  • email! - 邮箱格式
  • .default('value') - 默认值
  • .pattern(/regex/) - 正则验证

2. methods - 自定义方法

instance 方法

注入到查询返回的文档对象。

methods: (model) => ({
    instance: {
        checkPassword(password) {
            return this.password === password;  // this = 文档对象
        },
        isAdmin() {
            return this.role === 'admin';
        }
    }
})

// 使用
const user = await User.findOne({ username: 'test' });
user.checkPassword('secret123');  // ✅
user.isAdmin();                   // ✅

注意

  • ⚠️ 必须使用普通函数,不能用箭头函数
  • ⚠️ 方法名避免与字段名冲突,使用动词前缀:is*, check*, get*
  • ⚠️ 修改 this 不会自动保存到数据库

static 方法

挂载到 Model 实例。

methods: (model) => ({
    static: {
        async findByUsername(username) {
            return await model.findOne({ username });
        },
        async findAdmins() {
            return await model.find({ role: 'admin' });
        }
    }
})

// 使用
const User = msq.model('users');
const user = await User.findByUsername('test');  // ✅
const admins = await User.findAdmins();          // ✅

3. hooks - 生命周期钩子

在操作前后执行自定义逻辑。

hooks: (model) => ({
    insert: {
        before: async (ctx, docs) => {
            // 自动添加时间戳
            return { ...docs, createdAt: new Date() };
        },
        after: async (ctx, result) => {
            console.log('插入完成');
        }
    },
    update: {
        before: async (ctx, filter, update) => {
            if (!update.$set) update.$set = {};
            update.$set.updatedAt = new Date();
            return [filter, update];
        }
    }
})

支持的操作find, insert, update, delete

ctx 上下文:用于在 before 和 after 之间传递数据

before: async (ctx, docs) => {
    ctx.timestamp = Date.now();
},
after: async (ctx, result) => {
    console.log('耗时:', Date.now() - ctx.timestamp);
}

4. indexes - 自动创建索引

indexes: [
    { key: { username: 1 }, unique: true },      // 唯一索引
    { key: { status: 1, createdAt: -1 } },       // 复合索引
    { key: { expireAt: 1 }, expireAfterSeconds: 0 }  // TTL 索引
]

5. enums - 枚举配置

enums: {
    role: 'admin|user|guest',
    status: 'active|inactive'
}

// schema 中引用
schema: function(dsl) {
    return dsl({
        role: this.enums.role.default('user')
    });
}

完整示例

const MonSQLize = require('monsqlize');
const { Model } = MonSQLize;

// 定义 User Model
Model.define('users', {
    enums: {
        role: 'admin|user|guest',
        status: 'active|inactive|banned'
    },
    schema: function(dsl) {
        return dsl({
            username: 'string:3-32!',
            email: 'email!',
            password: 'string!'.pattern(/^[a-zA-Z0-9]{6,30}$/),
            role: this.enums.role.default('user'),
            status: this.enums.status.default('active'),
            loginCount: 'number'.default(0),
            lastLoginAt: 'date',
            createdAt: 'date!',
            updatedAt: 'date!'
        });
    },
    methods: (model) => ({
        instance: {
            checkPassword(password) {
                return this.password === password;
            },
            isAdmin() {
                return this.role === 'admin';
            },
            async incrementLogin() {
                return await model.updateOne(
                    { _id: this._id },
                    { 
                        $inc: { loginCount: 1 },
                        $set: { lastLoginAt: new Date() }
                    }
                );
            }
        },
        static: {
            async findByUsername(username) {
                return await model.findOne({ username });
            },
            async findActive() {
                return await model.find({ status: 'active' });
            },
            async countAdmins() {
                return await model.count({ role: 'admin' });
            }
        }
    }),
    hooks: (model) => ({
        insert: {
            before: async (ctx, docs) => {
                const now = new Date();
                return {
                    ...docs,
                    createdAt: now,
                    updatedAt: now
                };
            }
        },
        update: {
            before: async (ctx, filter, update) => {
                if (!update.$set) update.$set = {};
                update.$set.updatedAt = new Date();
                return [filter, update];
            }
        }
    }),
    indexes: [
        { key: { username: 1 }, unique: true },
        { key: { email: 1 }, unique: true },
        { key: { status: 1, createdAt: -1 } }
    ]
});

// 使用
const msq = new MonSQLize({
    type: 'mongodb',
    config: { uri: 'mongodb://localhost:27017/mydb' }
});
await msq.connect();

const User = msq.model('users');

// 创建用户
const result = User.validate({
    username: 'admin',
    email: 'admin@example.com',
    password: 'secret123',
    role: 'admin'
});

if (result.valid) {
    await User.insertOne(result.data);
}

// 登录验证
const user = await User.findByUsername('admin');
if (user && user.checkPassword('secret123')) {
    if (user.isAdmin()) {
        console.log('管理员登录');
    }
    await user.incrementLogin();
}

// 查询活跃用户
const activeUsers = await User.findActive();

// 统计管理员数量
const adminCount = await User.countAdmins();

注意事项

⚠️ 方法命名避免冲突

方法名不要与字段名相同,使用动词前缀。

// ❌ 错误
methods: { instance: { status() {} } }

// ✅ 正确
methods: { 
    instance: { 
        isActive() {},      // is* 判断
        checkStatus() {},   // check* 验证
        getFullName() {}    // get* 获取
    } 
}

⚠️ 必须使用普通函数

不能使用箭头函数,否则 this 指向错误。

// ❌ 错误
checkPassword: (password) => this.password === password

// ✅ 正确
checkPassword(password) { return this.password === password; }

⚠️ 修改不会自动保存

方法内修改 this 只改内存,不会保存到数据库。

// ❌ 错误:只改内存
updatePassword(pwd) { this.password = pwd; }

// ✅ 正确:调用更新方法
async changePassword(pwd) {
    return await model.updateOne(
        { _id: this._id },
        { $set: { password: pwd } }
    );
}

自动时间戳(v1.0.3+)

自动管理 createdAtupdatedAt 字段。

基本用法

Model.define('users', {
    schema: (dsl) => dsl({ username: 'string!' }),
    options: {
        timestamps: true  // 启用自动时间戳
    }
});

// 插入时自动添加
await User.insertOne({ username: 'john' });
// => { _id, username: 'john', createdAt: Date, updatedAt: Date }

// 更新时自动更新 updatedAt
await User.updateOne({ username: 'john' }, { $set: { status: 'active' } });
// => updatedAt 自动更新为当前时间

自定义字段名

Model.define('users', {
    options: {
        timestamps: {
            createdAt: 'created_time',  // 自定义创建时间字段名
            updatedAt: 'updated_time'   // 自定义更新时间字段名
        }
    }
});

部分启用

// 只启用 createdAt
Model.define('users', {
    options: {
        timestamps: {
            createdAt: true,
            updatedAt: false
        }
    }
});

// 只启用 updatedAt
Model.define('users', {
    options: {
        timestamps: {
            createdAt: false,
            updatedAt: true
        }
    }
});

支持的操作

操作 createdAt updatedAt 说明
insertOne 同时添加两个字段
insertMany 每个文档都添加
updateOne 只更新 updatedAt
updateMany 所有匹配文档更新
replaceOne 替换时更新
upsertOne ✅/❌ 插入时添加 createdAt,更新时不添加
findOneAndUpdate 只更新 updatedAt
findOneAndReplace 替换时更新
incrementOne ⚠️ 暂不支持

注意事项

⚠️ 用户手动设置会被覆盖

await User.insertOne({
    username: 'john',
    createdAt: new Date('2020-01-01')  // 会被覆盖
});
// => createdAt 会是当前时间,不是 2020-01-01

如需保留用户设置的值,请暂时禁用 timestamps 或在 before hook 中处理。

⚠️ incrementOne 暂不支持

// incrementOne 不会自动更新 updatedAt
await User.incrementOne({ _id }, { score: 10 });

临时方案:手动添加 updatedAt

await User.updateOne(
    { _id },
    { 
        $inc: { score: 10 },
        $set: { updatedAt: new Date() }
    }
);

✅ 与 schema 验证配合

timestamps 自动添加的字段会通过 schema 验证(如果 schema 中有定义)。

Model.define('users', {
    schema: (dsl) => dsl({
        username: 'string!',
        createdAt: 'date',    // 可选:定义验证规则
        updatedAt: 'date'
    }),
    options: {
        timestamps: true       // 自动添加的值会通过验证
    }
});

✅ 与 hooks 配合

timestamps 在用户 hooks 之后执行,不会影响用户的 before hook。

Model.define('users', {
    options: { timestamps: true },
    hooks: (model) => ({
        insert: {
            before: (ctx, docs) => {
                // 用户 hook 先执行
                return { ...docs, customField: 'value' };
            }
        }
    })
});

// 执行顺序:用户 before hook → timestamps → 数据库操作 → 用户 after hook

软删除(softDelete)

版本: v1.0.3+

软删除标记文档为已删除,而非物理删除,支持数据恢复和审计。

启用软删除

// 简单模式
Model.define('users', {
    schema: (dsl) => dsl({ username: 'string!' }),
    options: {
        softDelete: true  // 使用默认配置
    }
});

// 完整配置
Model.define('posts', {
    schema: (dsl) => dsl({ title: 'string!' }),
    options: {
        softDelete: {
            enabled: true,           // 启用软删除
            field: 'deletedAt',      // 字段名(可自定义)
            type: 'timestamp',       // 'timestamp' | 'boolean'
            ttl: 86400 * 30          // TTL 索引(30天后自动清理)
        }
    }
});

配置项

选项 类型 默认值 说明
enabled boolean true 启用软删除
field string 'deletedAt' 删除标记字段名
type string 'timestamp' 删除标记类型('timestamp' 或 'boolean')
ttl number null TTL 索引(秒),自动清理已删除数据

软删除操作

启用软删除后,deleteOnedeleteMany 会自动转换为更新操作:

const User = msq.model('users');

// 软删除(标记为已删除)
await User.deleteOne({ _id });
// 实际执行:updateOne({ _id }, { $set: { deletedAt: new Date() } })

// 批量软删除
await User.deleteMany({ status: 'inactive' });
// 实际执行:updateMany({ status: 'inactive' }, { $set: { deletedAt: new Date() } })

查询自动过滤

启用软删除后,查询操作自动过滤已删除的文档:

// 默认查询不返回已删除数据
const users = await User.find({});
// 实际执行:find({ deletedAt: null })

const user = await User.findOne({ username: 'john' });
// 实际执行:findOne({ username: 'john', deletedAt: null })

const count = await User.count({ status: 'active' });
// 实际执行:count({ status: 'active', deletedAt: null })

查询已删除数据

使用专门的方法查询包含或只查询已删除的数据:

// 查询包含已删除的所有数据
const allUsers = await User.findWithDeleted({});
const john = await User.findOneWithDeleted({ username: 'john' });
const totalCount = await User.countWithDeleted({});

// 只查询已删除的数据
const deletedUsers = await User.findOnlyDeleted({});
const deletedJohn = await User.findOneOnlyDeleted({ username: 'john' });
const deletedCount = await User.countOnlyDeleted({});

新增方法

方法 说明
findWithDeleted(filter, options) 查询包含已删除的文档
findOneWithDeleted(filter, options) 查询单个文档(包含已删除)
countWithDeleted(filter, options) 统计文档数(包含已删除)
findOnlyDeleted(filter, options) 只查询已删除的文档
findOneOnlyDeleted(filter, options) 查询单个已删除文档
countOnlyDeleted(filter, options) 统计已删除的文档数
restore(filter, options) 恢复已删除的文档
restoreMany(filter, options) 批量恢复已删除的文档
forceDelete(filter, options) 强制物理删除(不可恢复)
forceDeleteMany(filter, options) 批量强制物理删除

恢复已删除数据

// 恢复单个文档
const result = await User.restore({ _id });
// 实际执行:updateOne({ _id, deletedAt: { $ne: null } }, { $unset: { deletedAt: 1 } })

// 批量恢复
const result = await User.restoreMany({ status: 'active' });
// 实际执行:updateMany({ status: 'active', deletedAt: { $ne: null } }, { $unset: { deletedAt: 1 } })

强制物理删除

绕过软删除机制,执行真正的物理删除(不可恢复):

// 强制物理删除单个文档
await User.forceDelete({ _id });
// 实际执行:真正的 deleteOne(数据永久删除)

// 批量强制删除
await User.forceDeleteMany({ deletedAt: { $lt: thirtyDaysAgo } });
// 实际执行:真正的 deleteMany(批量永久删除)

删除类型

timestamp 类型(默认)

Model.define('users', {
    options: {
        softDelete: { type: 'timestamp' }  // 默认
    }
});

// 删除时记录删除时间
{ _id, username: 'john', deletedAt: new Date('2026-01-05T10:30:00Z') }

// 优点:记录删除时间,支持审计
// 缺点:占用存储空间

boolean 类型

Model.define('posts', {
    options: {
        softDelete: { type: 'boolean' }
    }
});

// 删除时标记为 true
{ _id, title: 'Hello', deletedAt: true }

// 优点:节省存储空间
// 缺点:不记录删除时间

自定义字段名

Model.define('comments', {
    options: {
        softDelete: {
            enabled: true,
            field: 'removed_at'  // 自定义字段名
        }
    }
});

// 删除时使用 removed_at 字段
await Comment.deleteOne({ _id });
// { _id, content: 'Nice!', removed_at: new Date() }

TTL 索引自动清理

配置 TTL 索引,MongoDB 会自动删除过期的已删除数据:

Model.define('logs', {
    options: {
        softDelete: {
            enabled: true,
            ttl: 86400 * 30  // 30天后自动清理
        }
    }
});

// 自动创建索引:
// db.logs.createIndex({ deletedAt: 1 }, { expireAfterSeconds: 2592000 })

// MongoDB 会自动删除 deletedAt 超过 30 天的文档

与 timestamps 协同

软删除和时间戳可以同时启用:

Model.define('products', {
    schema: (dsl) => dsl({ name: 'string!' }),
    options: {
        timestamps: true,   // 自动管理 createdAt/updatedAt
        softDelete: true    // 软删除
    }
});

// 插入时自动添加时间戳
await Product.insertOne({ name: 'iPhone' });
// { _id, name: 'iPhone', createdAt: Date, updatedAt: Date }

// 软删除时自动更新 updatedAt
await Product.deleteOne({ _id });
// { _id, name: 'iPhone', createdAt: Date, updatedAt: Date(更新), deletedAt: Date }

唯一索引处理

⚠️ 注意:软删除后,唯一索引可能失效。

// 问题:用户 john 被软删除后
{ username: 'john', deletedAt: new Date() }

// 创建新用户 john 会失败(唯一索引冲突)
await User.insertOne({ username: 'john' });  // ❌ 冲突

解决方案:使用复合唯一索引

Model.define('users', {
    schema: (dsl) => dsl({ username: 'string!' }),
    options: {
        softDelete: true
    },
    indexes: [
        {
            key: { username: 1, deletedAt: 1 },  // 复合索引
            unique: true
        }
    ]
});

// 现在可以创建同名用户(因为 deletedAt 不同)

乐观锁版本控制(Version)

什么是乐观锁?

乐观锁是一种并发控制机制,通过版本号检测数据冲突:

  • 每次更新时自动递增版本号
  • 更新时验证版本号是否匹配
  • 版本号不匹配说明数据已被其他请求修改(并发冲突)

使用场景

  • 多用户同时编辑同一数据
  • 防止脏写(Lost Update)
  • 需要并发安全保证的场景

基础配置

Model.define('users', {
    schema: (dsl) => dsl({
        username: 'string!',
        email: 'string!',
        status: 'string'
    }),
    options: {
        version: true  // 启用版本控制(默认字段名 version)
    }
});

完整配置

Model.define('users', {
    schema: (dsl) => dsl({
        username: 'string!',
        email: 'string!'
    }),
    options: {
        version: {
            enabled: true,      // 是否启用
            field: '__v'        // 自定义字段名(默认 'version')
        }
    }
});

插入时自动初始化

// 插入文档
const result = await User.insertOne({
    username: 'john',
    email: 'john@example.com'
});

// 查询文档
const user = await User.findOne({ _id: result.insertedId });
console.log(user);
// { _id: '...', username: 'john', email: 'john@example.com', version: 0 }

更新时自动递增

// 第一次更新
await User.updateOne({ _id }, { $set: { status: 'active' } });
// 实际执行:{ $set: { status: 'active' }, $inc: { version: 1 } }

const user = await User.findOne({ _id });
console.log(user.version);  // 1

// 第二次更新
await User.updateOne({ _id }, { $set: { status: 'inactive' } });
console.log(user.version);  // 2

并发冲突检测

// 用户 A 读取数据
const userA = await User.findOne({ _id });
console.log(userA.version);  // 0

// 用户 B 读取数据
const userB = await User.findOne({ _id });
console.log(userB.version);  // 0

// 用户 A 先更新成功
const resultA = await User.updateOne(
    { _id, version: userA.version },
    { $set: { status: 'active' } }
);
console.log(resultA.modifiedCount);  // 1

// 用户 B 更新失败
const resultB = await User.updateOne(
    { _id, version: userB.version },  // 版本号已过期
    { $set: { status: 'inactive' } }
);
console.log(resultB.modifiedCount);  // 0(版本号不匹配)

与其他功能协同

Model.define('users', {
    options: {
        timestamps: true,  // 自动时间戳
        softDelete: true,  // 软删除
        version: true      // 版本控制
    }
});

// 所有功能协同工作
await User.insertOne({ username: 'john' });
// { _id, username, version: 0, createdAt, updatedAt }

await User.deleteOne({ _id, version: 0 });
// 软删除时版本号递增

最佳实践

// 并发更新场景
async function updateUserStatus(userId, newStatus) {
    let maxRetries = 3;
    
    for (let i = 0; i < maxRetries; i++) {
        const user = await User.findOne({ _id: userId });
        if (!user) throw new Error('User not found');
        
        const result = await User.updateOne(
            { _id: userId, version: user.version },
            { $set: { status: newStatus } }
        );
        
        if (result.modifiedCount > 0) return { success: true };
        
        console.log(`Retry ${i + 1}/${maxRetries} (version conflict)`);
    }
    
    throw new Error('Update failed due to concurrent modification');
}

完整示例

const MonSQLize = require('monsqlize');
const { Model } = MonSQLize;

// 定义 Model(启用软删除和时间戳)
Model.define('articles', {
    schema: (dsl) => dsl({
        title: 'string!',
        content: 'string!',
        author: 'string!'
    }),
    options: {
        timestamps: true,
        softDelete: {
            enabled: true,
            type: 'timestamp',
            ttl: 86400 * 30  // 30天后自动清理
        }
    },
    indexes: [
        { key: { author: 1 } },
        { key: { title: 1, deletedAt: 1 }, unique: true }  // 复合唯一索引
    ]
});

async function example() {
    const msq = new MonSQLize({ type: 'mongodb', databaseName: 'blog' });
    await msq.connect();
    
    const Article = msq.model('articles');
    
    // 1. 插入文章
    const article = await Article.insertOne({
        title: 'Hello World',
        content: 'This is my first post',
        author: 'john'
    });
    console.log('Created:', article);
    // { _id, title, content, author, createdAt, updatedAt }
    
    // 2. 软删除文章
    await Article.deleteOne({ _id: article._id });
    console.log('Article soft deleted');
    
    // 3. 查询(自动过滤已删除)
    const articles = await Article.find({ author: 'john' });
    console.log('Active articles:', articles.length);  // 0
    
    // 4. 查询包含已删除
    const allArticles = await Article.findWithDeleted({ author: 'john' });
    console.log('All articles:', allArticles.length);  // 1
    
    // 5. 恢复文章
    await Article.restore({ _id: article._id });
    console.log('Article restored');
    
    // 6. 查询(恢复后可以查到)
    const restoredArticle = await Article.findOne({ _id: article._id });
    console.log('Restored:', restoredArticle.title);  // 'Hello World'
    
    // 7. 强制物理删除
    await Article.forceDelete({ _id: article._id });
    console.log('Article permanently deleted');
    
    await msq.close();
}

example().catch(console.error);

常见问题

Q: this.password 从哪来?
A: 来自数据库查询结果,不是 schema。schema 只定义验证规则。

Q: 如何引用 enums?
A: 使用 function 定义 schema:schema: function(dsl) { return dsl({ role: this.enums.role }) }

Q: instance 和 static 的区别?
A: instance 注入到文档对象,static 挂载到 Model 实例。


更多示例

查看 examples/model/ 目录:

  • basic.js - 基础使用
  • advanced.js - 高级功能