strapi/docs/3.x.x/en/guides/models.md
Aurélien GEORGET 37e989b52c
Update models.md
2018-06-20 18:50:49 +02:00

17 KiB

Models

See the models' concepts for details.

How to create a model?

Note: If you are just starting out it is very convenient to generate some models with the Content Type Builder, directly in the admin interface. You can then review the generated model mappings on the code level. The UI takes over a lot of validation tasks and gives you a fast feeling for available features.

Use the CLI, and run the following command strapi generate:model user firstname:string lastname:string. Read the CLI documentation for more informations.

This will create two files located at ./api/user/models:

  • User.settings.json: contains the list of attributes and settings. The JSON format makes the file easily editable.
  • User.js: imports User.settings.json and extends it with additional settings and lifecycle callbacks.

Note: when you create a new API using the CLI (strapi generate:api <name>), a model is automatically created.

Model Information

The info key on the model-json states information about the model. This information is used in the admin interface, when showing the model.

  • name: The name of the model, as shown in admin interface.
  • description: The description of the model.
  • mainField: Determines which model-attribute is shown when displaying the model.

Define the attributes

The following types are currently available:

  • string
  • text
  • integer
  • biginteger
  • float
  • decimal
  • password
  • date
  • time
  • datetime
  • timestamp
  • boolean
  • binary
  • uuid
  • enumeration
  • json
  • email

Validations

You can apply basic validations to the attributes. The following supported validations are only supported by MongoDB connection. If you're using SQL databases, you should use the native SQL constraints to apply them.

  • required (boolean) — if true adds a required validator for this property.
  • unique (boolean) — whether to define a unique index on this property.
  • max (integer) — checks if the value is greater than or equal to the given minimum.
  • min (integer) — checks if the value is less than or equal to the given maximum.

Security validations To improve the Developer eXperience when developing or using the administration panel, the framework enhances the attributes with these "security validations":

  • private (boolean) — if true, the attribute will be removed from the server response (it's useful to hide sensitive data).
  • configurable (boolean) - if false, the attribute isn't configurable from the Content Type Builder plugin.

Example

Path — User.settings.json.

{
  "connection": "default",
  "info": {
    "name": "user",
    "description": "This represents the User Model",
    "mainField": "email"
  },
  "attributes": {
    "firstname": {
      "type": "string"
    },
    "lastname": {
      "type": "string"
    },
    "email": {
      "type": "email",
      "required": true,
      "unique": true
    },
    "password": {
      "type": "password",
      "required": true,
      "private": true
    },
    "about": {
      "type": "description"
    },
    "age": {
      "type": "integer",
      "min": 18,
      "max": 99
    },
    "birthday": {
      "type": "date"
    }
  }
}

Relations

Refer to the relations concept for more informations about relations type.

One-way

Refer to the one-way concept for informations.

Example

A pet can be owned by someone (a user).

Path — ./api/pet/models/Pet.settings.json.

{
  "attributes": {
    "owner": {
      "model": "user"
    }
  }
}

Path — ./api/pet/controllers/Pet.js.

// Mongoose example
module.exports = {
  findPetsWithOwners: async (ctx) => {
    // Retrieve the list of pets with their owners.
    const pets = Pet
      .find()
      .populate('owner');

    // Send the list of pets.
    ctx.body = pets;
  }
}

One-to-one

Refer to the one-to-one concept for informations.

Example

A user can have one address. And this address is only related to this user.

Path — ./api/user/models/User.settings.json.

{
  "attributes": {
    "address": {
      "model": "address",
      "via": "user"
    }
  }
}

Path — ./api/address/models/Address.settings.json.

{
  "attributes": {
    "user": {
      "model": "user"
    }
  }
}

Path — ./api/user/controllers/User.js.

// Mongoose example
module.exports = {
  findUsersWithAddresses: async (ctx) => {
    // Retrieve the list of users with their addresses.
    const users = User
      .find()
      .populate('address');

    // Send the list of users.
    ctx.body = users;
  }
}

Path — ./api/adress/controllers/Address.js.

// Mongoose example
module.exports = {
  findArticlesWithUsers: async (ctx) => {
    // Retrieve the list of addresses with their users.
    const articles = Address
      .find()
      .populate('user');

    // Send the list of addresses.
    ctx.body = addresses;
  }
}

One-to-many

Refer to the one-to-many concept for more informations.

Example

A user can have many articles, and an article can be related to one user (author).

Path — ./api/user/models/User.settings.json.

{
  "attributes": {
    "articles": {
      "collection": "article",
      "via": "author"
    }
  }
}

Path — ./api/article/models/Article.settings.json.

{
  "attributes": {
    "author": {
      "model": "user"
    }
  }
}

Path — ./api/user/controllers/User.js.

// Mongoose example
module.exports = {
  findUsersWithArticles: async (ctx) => {
    // Retrieve the list of users with their articles.
    const users = User
      .find()
      .populate('articles');

    // Send the list of users.
    ctx.body = users;
  }
}

Path — ./api/article/controllers/Article.js.

// Mongoose example
module.exports = {
  findArticlesWithAuthors: async (ctx) => {
    // Retrieve the list of articles with their authors.
    const articles = Article
      .find()
      .populate('author');

    // Send the list of users.
    ctx.body = users;
  }
}

Many-to-many

Refer to the many-to-many concept.

Example

A product can be related to many categories, so a category can have many products.

Path — ./api/product/models/Product.settings.json.

{
  "attributes": {
    "categories": {
      "collection": "category",
      "via": "products",
      "dominant": true
    }
  }
}

Note: The dominant key allows you to define in which table/collection (only for NoSQL databases) should be stored the array that defines the relationship. Because there is no join table in NoSQL, this key is required for NoSQL databases (ex: MongoDB).

Path — ./api/category/models/Category.settings.json.

{
  "attributes": {
    "products": {
      "collection": "product",
      "via": "categories"
    }
  }
}

Path — ./api/product/controllers/Product.js.

// Mongoose example
module.exports = {
  findProductsWithCategories: async (ctx) => {
    // Retrieve the list of products.
    const products = Product
      .find()
      .populate('categories');

    // Send the list of products.
    ctx.body = products;
  }
}

Path — ./api/category/controllers/Category.js.

// Mongoose example
module.exports = {
  findCategoriesWithProducts: async (ctx) => {
    // Retrieve the list of categories.
    const categories = Category
      .find()
      .populate('products');

    // Send the list of categories.
    ctx.body = categories;
  }
}

Polymorphic

The polymorphic relationships are the solution when you don't know which kind of model will be associated to your entry. A common use case is an Image model that can be associated to many others kind of models (Article, Product, User, etc).

Refer to the upload plugin polymorphic implementation for more informations.

Single vs Many

Let's stay with our Image model which might belongs to a single Article or Product entry.

In other words, it means that a Image entry can be associated to one entry. This entry can be a Article or Product entry.

Path — ./api/image/models/Image.settings.json.

{
  "attributes": {
    "related": {
      "model": "*",
      "filter": "field"
    }
  }
}

Also, our Image model which might belongs to many Article or Product entries.

In other words, it means that a Article entry can relate to the same image than a Product entry.

Path — ./api/image/models/Image.settings.json.

{
  "attributes": {
    "related": {
      "collection": "*",
      "filter": "field"
    }
  }
}

Filter

The filter attribute is optional (but we highly recommend to use every time). If it's provided it adds a new match level to retrieve the related data.

For example, the Product model might have two attributes which are associated to the Image model. To distinguish which image is attached to the cover field and which images are attached to the pictures field, we need to save and provide this to the database.

Path — ./api/article/models/Product.settings.json.

{
  "attributes": {
    "cover": {
      "model": "image",
      "via": "related",
    },
    "pictures": {
      "collection": "image",
      "via": "related"
    }
  }
}

The value is the filter attribute is the name of the column where the information is stored.

Example

A Image model might belongs to many either Article models or a Product models.

Path — ./api/image/models/Image.settings.json.

{
  "attributes": {
    "related": {
      "collection": "*",
      "filter": "field"
    }
  }
}

Path — ./api/article/models/Article.settings.json.

{
  "attributes": {
    "avatar": {
      "model": "image",
      "via": "related"
    }
  }
}

Path — ./api/article/models/Product.settings.json.

{
  "attributes": {
    "pictures": {
      "collection": "image",
      "via": "related"
    }
  }
}

Path — ./api/image/controllers/Image.js.

// Mongoose example
module.exports = {
  findFiles: async (ctx) => {
    // Retrieve the list of images with the Article or Product entries related to them.
    const images = Images
      .find()
      .populate('related');

    /*
    [{
      "_id": "5a81b0fa8c063a53298a934a",
      "url": "http://....",
      "name": "john_doe_avatar.png",
      "related": [{
        "_id": "5a81b0fa8c063a5393qj934a",
        "title": "John Doe is awesome",
        "description": "..."
      }, {
        "_id": "5a81jei389ns5abd75f79c",
        "name": "A simple chair",
        "description": "..."
      }]
    }]
    */

    // Send the list of files.
    ctx.body = images;
  }
}

Path — ./api/article/controllers/Article.js.

// Mongoose example
module.exports = {
  findArticlesWithAvatar: async (ctx) => {
    // Retrieve the list of articles with the avatar (image).
    const articles = Article
      .find()
      .populate('avatar');

    /*
    [{
      "_id": "5a81b0fa8c063a5393qj934a",
      "title": "John Doe is awesome",
      "description": "...",
      "avatar": {
        "_id": "5a81b0fa8c063a53298a934a",
        "url": "http://....",
        "name": "john_doe_avatar.png"
      }
    }]
    */

    // Send the list of users.
    ctx.body = articles;
  }
}

Path — ./api/product/controllers/Product.js.

// Mongoose example
module.exports = {
  findProductWithPictures: async (ctx) => {
    // Retrieve the list of products with the pictures (images).
    const products = Product
      .find()
      .populate('pictures');

    /*
    [{
      "_id": "5a81jei389ns5abd75f79c",
      "name": "A simple chair",
      "description": "...",
      "pictures": [{
        "_id": "5a81b0fa8c063a53298a934a",
        "url": "http://....",
        "name": "chair_position_1.png"
      }, {
        "_id": "5a81d22bee1ad45abd75f79c",
        "url": "http://....",
        "name": "chair_position_2.png"
      }, {
        "_id": "5a81d232ee1ad45abd75f79e",
        "url": "http://....",
        "name": "chair_position_3.png"
      }]
    }]
    */

    // Send the list of users.
    ctx.body = products;
  }
}

Database implementation

If you're using MongoDB as a database, you don't need to do anything. Everything is natively handled by Strapi. However, to implement a polymorphic relationship with SQL databases, you need to create two tables.

Path — ./api/image/models/Image.settings.json.

{
  "attributes": {
    "name": {
      "type": "string"
    },
    "url": {
      "type": "string"
    },
    "related": {
      "collection": "*",
      "filter": "field"
    }
  }
}

The first table to create is the table which has the same name as your model.

CREATE TABLE `image` (
  `id` int(11) NOT NULL,
  `name` text NOT NULL,
  `text` text NOT NULL
)

Note: If you've overrided the default table name given by Strapi by using the collectionName attribute. Use the value set in the collectionName to name the table.

The second table will allow us to associate one or many others entries to the Image model. The name of the table is the same as the previous one with the suffix _morph.

CREATE TABLE `image_morph` (
  `id` int(11) NOT NULL,
  `image_id` int(11) NOT NULL,
  `related_id` int(11) NOT NULL,
  `related_type` text NOT NULL,
  `field` text NOT NULL
)
  • image_id is using the name of the first table with the suffix _id.
    • Attempted value: It correspond to the id of an Image entry.
  • related_id is using the attribute name where the relation happens with the suffix _id.
    • Attempted value: It correspond to the id of an Article or Product entry.
  • related_type is using the attribute name where the relation happens with the suffix _type.
    • Attempted value: It correspond to the table name where the Article or Product entry is stored.
  • field is using the filter property value defined in the model. If you change the filter value, you have to change the name of this column as well.
    • Attempted value: It correspond to the attribute of a Article, Product with which the Image entry is related.
id image_id related_id related_type field
1 1738 39 product cover
2 4738 58 article avatar
3 1738 71 article avatar

Lifecycle callbacks

Refer to the lifecycle callbacks concepts for informations.

The following events are available by default:

Callbacks on save:

  • beforeSave
  • afterSave

Callbacks on fetch:

  • beforeFetch
  • afterFetch

Callbacks on fetchAll:

  • beforeFetchAll
  • afterFetchAll

Callbacks on create:

  • beforeCreate
  • afterCreate

Callbacks on update:

  • beforeUpdate
  • afterUpdate

Callbacks on destroy:

  • beforeDestroy
  • afterDestroy

Mongoose

The entry is available through the model parameter

Path — ./api/user/models/User.js.

module.exports = {
 /**
  * Triggered before user creation.
  */
 beforeCreate: async (model) => {
   // Hash password.
   const passwordHashed = await strapi.api.user.services.user.hashPassword(model.password);

   // Set the password.
   model.password = passwordHashed;
 }
}

Bookshelf

Each of these functions receives a three parameters model, attrs and options. You have to return a Promise.

Path — ./api/user/models/User.js.

module.exports = {

  /**
   * Triggered before user creation.
   */
  beforeCreate: async (model, attrs, options) => {
    // Hash password.
    const passwordHashed = await strapi.api.user.services.user.hashPassword(model.attributes.password);

    // Set the password.
    model.set('password', passwordHashed);
  }
}

Settings

Additional settings can be set on models:

  • connection (string) - Connection's name which must be used. Default value: default.
  • collectionName (string) - Collection's name (or table's name) in which the data should be stored.
  • globalId (string) -Global variable name for this model (case-sensitive).

Path — User.settings.json.

{
  "connection": "mongo",
  "collectionName": "Users_v1",
  "globalId": "Users",
  "attributes": {

  }
}

In this example, the model User will be accessible through the Users global variable. The data will be stored in the Users_v1 collection or table and the model will use the mongo connection defined in ./config/environments/**/database.json

Note: The connection value can be changed whenever you want, but you should be aware that there is no automatic data migration process. Also if the new connection doesn't use the same ORM you will have to rewrite your queries.