模型

页面摘要: 模型通过内容类型和可复用组件来定义 Strapi 的内容结构。本文档逐步介绍了如何在内容类型构建器或 CLI 中创建这些模型,以及如何使用可选的生命周期钩子管理模式文件。

由于 Strapi 是一个无头内容管理系统(CMS),为内容创建内容结构是使用该软件最重要的方面之一。模型定义了内容结构的表示形式。

Strapi 中有 2 种不同类型的模型:

  • 内容类型,根据它们管理的条目数量,可以是集合类型或单一类型,
  • 以及可以在多个内容类型中复用的组件。

如果你刚刚开始,使用管理面板中的内容类型构建器生成一些模型会很方便。用户界面接管了许多验证任务,并展示了创建内容结构时可用的所有选项。然后可以使用本文档在代码层面查看生成的模型映射。

模型创建

内容类型和组件模型的创建和存储方式不同。

内容类型

Strapi 中的内容类型可以通过以下方式创建:

内容类型使用以下文件:

  • schema.json 用于模型的模式定义。(无论使用哪种方法创建内容类型,都会自动生成)
  • lifecycles.js 用于生命周期钩子。此文件必须手动创建。

这些模型文件存储在 ./src/api/[api-name]/content-types/[content-type-name]/ 中,在这些文件夹中找到的任何 JavaScript 或 JSON 文件都会被加载为内容类型的模型(请参阅项目结构)。

内容类型的存储方式不取决于它们在管理面板中的显示方式。内容类型可以分组到文件夹中,这些文件夹在单独的内容结构文件中进行描述。

TIP

在启用了 TypeScript 的项目中,可以使用 ts:generate-types 命令生成模式类型定义。

组件 {#components-creation}

组件模型无法通过 CLI 工具创建。请使用内容类型构建器或手动创建它们。

组件模型存储在 ./src/components 文件夹中。每个组件都必须位于一个以该组件所属类别命名的子文件夹内(请参阅项目结构)。

组件也接受一个可选的预览图片,在动态区域选择器中显示 以替代其图标(请参阅组件预览图片)。

模型模式

模型的 schema.json 文件由以下部分组成:

  • 设置,例如模型所表示的内容类型种类,或数据应存储的表名,
  • 信息,主要用于在管理面板中显示模型并通过 REST 和 GraphQL API 访问它,
  • 属性,用于描述模型的内容结构,
  • 以及用于在模型上定义特定行为的选项。

模型设置

模型的通用设置可以使用以下参数进行配置:

参数类型描述
collectionNameString数据应存储的数据库表名
kind

可选, 仅适用于内容类型 | String | 定义内容类型是:

  • 集合类型(collectionType)
  • 或单一类型(singleType) |
// ./src/api/[api-name]/content-types/restaurant/schema.json

{
  "kind": "collectionType",
  "collectionName": "Restaurants_v1",
}

模型信息

模型模式中的 info 键描述了用于在管理面板中显示模型并通过内容 API 访问它的信息。它包含以下参数:

参数类型描述
displayNameString在管理面板中使用的默认名称
singularNameString内容类型名称的单数形式。

用于生成 API 路由以及数据库/表集合。

应为 kebab-case(短横线命名)。 | | pluralName | String | 内容类型名称的复数形式。 用于生成 API 路由以及数据库/表集合。

应为 kebab-case。 | | description | String | 模型的描述 | | icon | String | 用于在管理面板中表示模型的 Strapi 图标 名称 | | preview | String | 用于在管理面板中表示组件图片的路径或 URL。

仅适用于组件。请参阅 组件预览图片。 |

NOTE

此 preview 参数与预览功能无关。 该功能用于预览前端内容,使用的是 config/admin 的 preview 对象。


  "info": {
    "displayName": "Restaurant",
    "singularName": "restaurant",
    "pluralName": "restaurants",
    "description": ""
  },

组件预览图片

组件在其 info 对象中接受一个可选的 preview 参数。它指向一张用于在动态区域选择器中表示组件的图片。

preview 参数接受:

  • 指向放置在项目 public 目录中的图片的根相对路径,例如 /_component-screenshots/hero-section.png。该图片由 public 中间件 提供,它不会提供以 /uploads/ 开头的路径。
  • 指向外部图片主机的绝对 URL。
WARNING

媒体库图片是从 /uploads/ 提供的,因此它们不能用作预览图片。请将文件提交到 public 目录中。

{
  "info": {
    "displayName": "Hero Section",
    "icon": "layout",
    "preview": "/_component-screenshots/hero-section.png"
  }
}

当省略 preview,或图片加载失败时,管理面板会回退到组件的 icon。

NOTE

preview 参数必须在组件的架构文件中手动设置。内容类型构建器目前还无法上传预览图片。

模型属性

模型的内容结构由一组属性组成。每个属性都有一个 type 参数,用于描述其性质,并将该属性定义为简单的数据片段或 Strapi 使用的更复杂的结构。

可以使用多种类型的属性:

  • 标量类型(例如字符串、日期、数字、布尔值等),
  • Strapi 特定的类型,例如:

属性的 type 参数应为以下值之一:

类型类别可用类型
字符串类型
  • string

  • text

  • richtext

  • enumeration

  • email

  • password

  • uid | | 日期类型 |

  • date

  • time

  • datetime

  • timestamp | | 数字类型 |

  • integer

  • biginteger

  • float

  • decimal | | 其他通用类型 |

  • boolean

  • json | | Strapi 特有的特殊类型 |

  • media

  • relation

  • customField

  • component

  • dynamiczone | | 国际化(i18n)相关类型

仅当内容类型上启用了 i18n 时才能使用|

  • locale
  • localizations |

验证

可以使用以下参数对属性应用基础验证:

参数类型描述默认值
requiredBoolean如果为 true,则为此属性添加必填验证器false
maxInteger检查值是否大于或等于给定的最大值-
minInteger检查值是否小于或等于给定的最小值-
minLengthInteger字段输入值的最小字符数-
maxLengthInteger字段输入值的最大字符数-
privateBoolean如果为 true,该属性将从服务器响应中移除。

💡 这可用于隐藏敏感数据。 | false | | configurable | Boolean | 如果为 false,则该属性无法从内容类型构建器插件进行配置。 | true |


{
  // ...
  "attributes": {
    "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 99,
      "unique": true
    },
    "description": {
      "default": "My description",
      "type": "text",
      "required": true
    },
    "slug": {
      "type": "uid",
      "targetField": "title"
    }
    // ...
  }
}

数据库验证与设置

🚧 此 API 被视为实验性。

这些设置应保留给高级用法,因为它们可能会破坏某些功能。目前没有计划让这些设置变得稳定。

数据库验证与设置是在模式迁移期间直接传递给 tableBuilder Knex.js 函数的自定义选项。数据库验证允许在设置自定义列设置方面实现高级程度的控制。以下选项按属性设置在 column: {} 对象中:

参数类型描述默认值
namestring更改数据库中列的名称-
defaultTostring设置数据库的 defaultTo,通常与 notNullable 一起使用-
notNullableboolean设置数据库的 notNullable,确保列不能为 nullfalse
unsignedboolean仅适用于数字列,取消取负值的能力,但将最大长度翻倍false
uniqueboolean对已发布的条目强制实施数据库级别的惟一性。当启用草稿与发布(Draft & Publish)时,草稿保存会跳过检查,因此重复项仅在发布时才会失败。false
typestring更改数据库类型,如果 type 带有参数,应在 args 中传递它们-
argsarray传递给用于更改 type 等内容的 Knex.js 函数的参数[]
草稿与发布(Draft & Publish)与 `unique`

当启用草稿与发布时,Strapi 会刻意跳过正在保存为草稿的条目的 unique 验证。因此,重复项在发布之前一直不会被检测到,此时即使 UI 此前为草稿显示过"已保存文档",数据库约束也会触发错误。

为避免意外的发布失败:

  • 在必须保持全局唯一的内容类型上禁用草稿与发布,
  • 或添加自定义验证(例如生命周期钩子或中间件),在保存之前检查草稿重复项,
  • 或依赖自动生成的唯一标识符,例如 uid 字段以及文档编辑规范。

{
  // ...
  "attributes": {
    "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 99,
      "unique": true,
      "column": {
        "unique": true // enforce database unique also
      }
    },
    "description": {
      "default": "My description",
      "type": "text",
      "required": true,
      "column": {
        "defaultTo": "My description", // set database level default
        "notNullable": true // enforce required at database level, even for drafts
      }
    },
    "rating": {
      "type": "decimal",
      "default": 0,
      "column": {
        "defaultTo": 0,
        "type": "decimal", // using the native decimal type but allowing for custom precision
        "args": [
          6,1 // using custom precision and scale
        ]
      }
    }
    // ...
  }
}

uid 类型

uid 类型用于根据 2 个可选参数,在管理面板中自动预填字段值为唯一标识符(UID)(例如文章的别名 slug):

  • targetField(字符串):如果使用了它,则用作目标的字段的值会被用来自动生成 UID。
  • options(字符串):如果使用了它,则 UID 是基于传递给 底层 uid 生成器 的一组选项生成的。生成的 uid 必须匹配以下正则表达式模式:/^[A-Za-z0-9-_.~]*$。

关联关系

关联关系将内容类型链接在一起。Strapi 同时支持单条目关联(单向和一对一)以及多处关联(其中至少一侧可以指向多个条目,包括一对多、多对一、多对多和多向)。多处关联在数据库层以数组形式持久化,并在内容 API 响应中作为数组返回。

关联通过在模型的属性中显式定义 type: 'relation' 来声明,并接受以下附加参数:

参数描述
relation以下值之一的关联类型:
  • oneToOne
  • oneToMany
  • manyToOne
  • manyToMany | | target | 接受字符串值作为目标内容类型的名称 | | mappedBy 和 inversedBy

可选 | 在双向关联中,拥有方声明 inversedBy 键,而被反向方声明 mappedBy 键 |

一对一

一对一关系适用于一个条目只能链接到另一个条目的情况。

它们可以是单向的或双向的。在单向关系中,只有其中一个模型可以与其关联项一起被查询。

单向用例示例:

  • 一篇博客文章属于一个分类。
  • 查询文章可以获取其分类,
  • 但查询分类不会获取其所属的文章。

  // …
  attributes: {
    category: {
      type: 'relation',
      relation: 'oneToOne',
      target: 'category',
    },
  },
  // …

双向用例示例:

  • 一篇博客文章属于一个分类。
  • 查询文章可以获取其分类,
  • 并且查询分类也会获取其所属的文章。

  // …
  attributes: {
    category: {
      type: 'relation',
      relation: 'oneToOne',
      target: 'category',
      inversedBy: 'article',
    },
  },
  // …


  // …
  attributes: {
    article: {
      type: 'relation',
      relation: 'oneToOne',
      target: 'article',
      mappedBy: 'category',
    },
  },
  // …

一对多

一对多关系适用于以下情况:

  • 内容类型 A 的一个条目链接到另一个内容类型 B 的多个条目,
  • 而内容类型 B 的一个条目只链接到内容类型 A 的一个条目。

一对多关系始终是双向的,并且通常与相应的一对多(应为多对一)关系一起定义:

示例: 一个人可以拥有很多植物,但一株植物只被一个人拥有。


  // …
  attributes: {
    owner: {
      type: 'relation',
      relation: 'manyToOne',
      target: 'api::person.person',
      inversedBy: 'plants',
    },
  },
  // …


  // …
  attributes: {
    plants: {
      type: 'relation',
      relation: 'oneToMany',
      target: 'api::plant.plant',
      mappedBy: 'owner',
    },
  },
  // …

多对一

多对一关系适用于将多个条目链接到一个条目的情况。

它们可以是单向的或双向的。在单向关系中,只有其中一个模型可以与其关联项一起被查询。

单向用例示例:

一本书可以由多位作者合著。


  // …
  attributes: {
    author: {
      type: 'relation',
      relation: 'manyToOne',
      target: 'author',
    },
  },
  // …

双向用例示例:

一篇文章只属于一个分类,但一个分类有多篇文章。


  // …
  attributes: {
    author: {
      type: 'relation',
      relation: 'manyToOne',
      target: 'category',
      inversedBy: 'article',
    },
  },
  // …

  // …
  attributes: {
    books: {
      type: 'relation',
      relation: 'oneToMany',
      target: 'article',
      mappedBy: 'category',
    },
  },
  // …

多对多

多对多关系适用于以下情况:

  • 内容类型 A 的一个条目链接到内容类型 B 的多个条目,
  • 并且内容类型 B 的一个条目也链接到内容类型 A 的多个条目。

多对多关系可以是单向的或双向的。在单向关系中,只有其中一个模型可以与其关联项一起被查询。

单向用例示例:

  // …
  attributes: {
    categories: {
      type: 'relation',
      relation: 'manyToMany',
      target: 'category',
    },
  },
  // …

双向用例示例:

一篇文章可以有多个标签,而一个标签可以被分配给多篇文章。


  // …
  attributes: {
    tags: {
      type: 'relation',
      relation: 'manyToMany',
      target: 'tag',
      inversedBy: 'articles',
    },
  },
  // …

  // …
  attributes: {
    articles: {
      type: 'relation',
      relation: 'manyToMany',
      target: 'article',
      mappedBy: 'tag',
    },
  },
  // …

自定义字段

自定义字段通过向内容类型添加新类型的字段来扩展 Strapi 的能力。自定义字段通过在模型的属性中显式定义 type: customField 来声明。

自定义字段属性还表现出以下特性:

  • 一个 customField 属性,其值作为唯一标识符,用于指示应使用哪个已注册的自定义字段。其值遵循以下格式:
    • 如果自定义字段由插件创建,则为 plugin::plugin-name.field-name 格式
    • 或用于当前 Strapi 应用特定的自定义字段的 global::field-name 格式
  • 以及取决于注册自定义字段时定义的附加参数(请参阅自定义字段文档)。

{
  // …
  "attributes": {
    "attributeName": { // attributeName 将替换为实际的属性名称
      "type": "customField",
      "customField": "plugin::color-picker.color",
      "options": {
        "format": "hex"
      }
    }
  }
  // …
}

组件 {#components-json}

组件字段在内容类型和组件结构之间创建关联。组件通过在模型的属性中显式定义 type: 'component' 来声明,并接受以下附加参数:

参数类型描述
repeatableBoolean根据组件是否可重复,值可以是 true 或 false
componentString定义对应的组件,遵循以下格式:
<category>.<componentName>

{
  "attributes": {
    "openinghours": {
      "type": "component",
      "repeatable": true,
      "component": "restaurant.openinghours"
    }
  }
}

这些参数设置在使用该组件的内容类型的属性上。 设置在组件本身的参数(例如其预览图片)属于它自己的 info 对象(请参阅组件预览图片)。

动态区域

动态区域创建了一个灵活的空间,用于基于混合的组件列表来组合内容。

动态区域通过在模型的属性中显式定义 type: 'dynamiczone' 来声明。它们还接受一个 components 数组,其中每个组件都应遵循以下格式命名:<category>.<componentName>。


{
  "attributes": {
    "body": {
      "type": "dynamiczone",
      "components": ["article.slider", "article.content"]
    }
  }
}

模型选项

options 键用于定义特定行为,并接受以下参数:

参数类型描述
privateAttributesArray of strings允许将一组属性视为私有,即使它们实际上并未在模型中定义为属性。它可以用于从 API 响应中移除它们(例如时间戳)。

模型中定义的 privateAttributes 会与全局 Strapi 配置中定义的 privateAttributes 合并。 | | draftAndPublish | Boolean | 启用草稿与发布功能。

默认值:true(如果使用交互式 CLI 创建内容类型则为 false)。 | | populateCreatorFields | Boolean | 在 REST API 返回的响应中填充 createdBy 和 updatedBy 字段(更多详情请参阅指南)。

默认值:false。 |


{
  "options": {
    "privateAttributes": ["id", "createdAt"],
    "draftAndPublish": true
  }
}

插件选项

pluginOptions 是一个可选对象,允许插件为模型或特定属性存储配置。

键值描述
i18nlocalized: true启用本地化。
content-managervisible: false在管理面板中从内容管理器隐藏。
content-type-buildervisible: false在管理面板中从内容类型构建器隐藏。

{
  "attributes": {
    "name": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "string",
      "required": true
    },
    "slug": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "uid",
      "targetField": "name",
      "required": true
    }
    // …additional attributes
  }
}

生命周期钩子

生命周期钩子是在调用 Strapi 查询时触发的函数。当通过管理面板管理内容或使用 queries 开发自定义代码时,它们会自动触发。

生命周期钩子可以通过声明式或编程式进行自定义。

WARNING

当直接使用 knex 库而不是 Strapi 函数时,不会触发生命周期钩子。

文档服务 API:生命周期钩子与中间件

文档服务 API 会根据调用的方法触发各种数据库生命周期钩子。有关完整参考,请参阅文档服务 API:生命周期钩子。批量操作的 liftcycle 钩子(createMany、updateMany、deleteMany)永远不会被文档服务 API 方法触发。文档服务中间件 也可以被实现。

可用的生命周期事件

提供以下生命周期事件:

  • beforeCreate
  • beforeCreateMany
  • afterCreate
  • afterCreateMany
  • beforeUpdate
  • beforeUpdateMany
  • afterUpdate
  • afterUpdateMany
  • beforeDelete
  • beforeDeleteMany
  • afterDelete
  • afterDeleteMany
  • beforeCount
  • afterCount
  • beforeFindOne
  • afterFindOne
  • beforeFindMany
  • afterFindMany

钩子的 event 对象

生命周期钩子是接收 event 参数的函数,该对象包含以下键:

键类型描述
actionString已触发的生命周期事件(请参阅列表)
modelArray of strings (uid)要监听其事件的 content-types 的 uid 数组。
如果未提供此参数,则监听所有内容类型的事件。
paramsObject接受以下参数:
  • data
  • select
  • where
  • orderBy
  • limit
  • offset
  • populate | | result | Object | 可选,仅适用于 afterXXX 事件

包含操作的结果。 | | state | Object | 查询状态,可用于在查询的 beforeXXX 和 afterXXX 事件之间共享状态。 |

声明式与编程式用法

要配置内容类型的生命周期钩子,请在 ./src/api/[api-name]/content-types/[content-type-name]/ 文件夹中创建一个 lifecycles.js 文件。

每个事件监听器按顺序调用。它们可以是同步的或异步的。

JavaScript


module.exports = {
  beforeCreate(event) {
    const { data, where, select, populate } = event.params;

    // let's do a 20% discount everytime
    event.params.data.price = event.params.data.price * 0.8;
  },

  afterCreate(event) {
    const { result, params } = event;

    // do something to the result;
  },
};

TypeScript


export default {
  beforeCreate(event) {
    const { data, where, select, populate } = event.params;

    // let's do a 20% discount everytime
    event.params.data.price = event.params.data.price * 0.8;
  },

  afterCreate(event) {
    const { result, params } = event;

    // do something to the result;
  },
};

使用数据库层 API,也可以注册一个订阅者并以编程方式监听事件:

module.exports = {
  async bootstrap({ strapi }) {
// registering a subscriber
    strapi.db.lifecycles.subscribe({
      models: [], // optional;

      beforeCreate(event) {
        const { data, where, select, populate } = event.params;

        event.state = 'doStuffAfterWards';
      },

      afterCreate(event) {
        if (event.state === 'doStuffAfterWards') {
        }

        const { result, params } = event;

        // do something to the result
      },
    });

    // generic subscribe for generic handling
    strapi.db.lifecycles.subscribe((event) => {
      if (event.action === 'beforeCreate') {
        // do something
      }
    });
  }
}