Just to remind you of the last article, where we talked about

  • Service
  • The plug-in
  • Timing task
  • Framework extension
  • Enabling Customization
  • Application deployment
  • The log
  • HttpClient

The Cookie and Session

Cookie

Through CTx. cookies, we can conveniently and securely set and read cookies in controller.

class HomeController extends Controller {
  async add() {
    const ctx = this.ctx;
    let count = ctx.cookies.get('count');
    count = count ? Number(count) : 0;
    ctx.cookies.set('count', ++count);
    ctx.body = count;
  }
  async remove() {
    const ctx = this.ctx;
    ctx.cookies.set('count'.null);
    ctx.status = 204; }}Copy the code

ctx.cookies.set(key, value, options)

Setting a Cookie is done by setting the set-cookie header in the HTTP response. Each set-cookie causes the browser to store a key-value pair in the Cookie. In addition to setting the Cookie value, the protocol supports a number of parameters to configure the Cookie’s transmission, storage, and permissions.

  • {Number} maxAge: Sets the maximum duration of the key-value pair in the browser. Is the number of milliseconds from the current time of the server.
  • {Date} expires: Sets an expiration time for the key-value pair. If maxAge is set, expires will be overwritten. If maxAge and Expires are not set, cookies will expire when the browser’s session expires (usually when the browser is closed).
  • {String} path: Sets the effective URL path of the key-value pair. The default setting is on the root path (/), that is, all urls under the current domain name can access this Cookie.
  • {String} domain: Specifies the domain name for which the key pair takes effect. By default, the domain name is not configured and can be accessed only in the specified domain name.
  • {Boolean} httpOnly: Sets whether key/value pairs can be accessed by JS. The default value is true and js is not allowed to access them.
  • {Boolean} secure: Sets key/value pairsTransfer only over HTTPS connectionsThe framework will help us determine whether secure is currently set automatically on HTTPS connections.

In addition to these attributes, the framework extends support for three additional parameters:

  • {Boolean} overwrite: How to handle key-value pairs with the same key, if set totrue, the latter value overrides the previous one, otherwise two set-cookie response headers will be sent.
  • {Boolean} signed: Indicates whether to sign cookies. If this parameter is set to true, the value of the key-value pair will be signed at the same time when the key-value pair is set, and the value will be verified when the key-value pair is set to prevent the front-end from tampering with the value. The default is true.
  • {Boolean} encrypt: Indicates whether to encrypt cookies. If this parameter is set to true, the value of the key-value pair is encrypted before sending cookies. The client cannot read the plaintext value of cookies. The default is false.

Cookies are signed and not encrypted, so the browser can see the plaintext, js cannot be accessed, and cannot be tampered with by the client (manually).

If you want cookies to be accessible and modified by JS on the browser side:

ctx.cookies.set(key, value, {
  httpOnly: false.signed: false});Copy the code
  • Due to the uncertainty of the implementation of browsers and other clients, in order to ensure that cookies can be written successfully, it is recommended that value be written after base64 encoding or other forms of encode.
  • Because browsers have limits on the length of cookies, try not to set cookies that are too long. Generally, do not exceed 4093 bytes. When the Cookie value is greater than this value, the framework prints a warning log.

ctx.cookies.get(key, options)

When setting the Cookie, you can set options.signed and options.encrypt to sign or encrypt the Cookie. Therefore, the corresponding option must be passed when obtaining the Cookie.

  • If the value is specified as signed when it is set but not specified when it is acquired, the value will not be checked when it is acquired, which may be tampered with by the client.
  • If encrypt is specified when you set it to encrypt but not specified when you obtain it, the encrypted ciphertext cannot be obtained.

Cookie secret key

Since we need to use encryption, decryption and verification in cookies, we need to configure a secret key for encryption. In the config/config. Default. Js

module.exports = {
  keys: 'key1,key2'};Copy the code

Keys are configured as a string, and you can configure multiple keys separated by commas. Cookie when using this configuration for encryption and decryption:

  • Only the first secret key is used for encryption and signing.
  • Keys are traversed for decryption and verification.

If we want to update the secret key of the Cookie, but do not want the Cookie previously set on the user’s browser to be invalid, we can configure the new secret key to the front of the keys, and delete the unnecessary secret key after a period of time.

Session

Cookie often bears the function of identifying the requestor in Web applications, so Web applications encapsulate the concept of Session on the basis of Cookie, which is specially used for user identification.

The framework has a built-in Session plugin, which provides ctx. Session to access or modify the current user Session.

class HomeController extends Controller {
  async fetchPosts() {
    const ctx = this.ctx;
    // Get the contents of Session
    const userId = ctx.session.userId;
    const posts = await ctx.service.post.fetch(userId);
    // Change the Session value
    ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
    ctx.body = {
      success: true, posts, }; }}Copy the code

Session is a very straightforward way to use it, just read it or modify it, if you want to delete it, just assign it null:

ctx.session = null;
Copy the code

Note that: When setting the session attribute, avoid the following situations (which may cause field loss, refer to the KOA-Session source code) :

  • Don’t want to_At the beginning
  • Can’t forisNew
// ❌ incorrect usage
ctx.session._visited = 1;   // --> This field will be lost on the next request
ctx.session.isNew = 'HeHe'; // --> is an internal keyword and should not be changed

// ✔️ correct usage
ctx.session.visited = 1;    // --> No problem here
Copy the code

The implementation of Session is based on Cookie. By default, the content of user Session is encrypted and directly stored in a field of Cookie. Users will bring this Cookie every time they request our website, and we will use it after decryption on the server. The default Session configuration is as follows:

exports.session = {
  key: 'EGG_SESS'.maxAge: 24 * 3600 * 1000./ / 1 day
  httpOnly: true.encrypt: true};Copy the code

As you can see, these parameters are all Cookie parameters except key, which represents the key of the Cookie key-value pair that stores the Session. In the default configuration, the cookies that store the Session are encrypted and cannot be accessed by the front-end JS to ensure the security of the user Session.

Extended stored

Sessions are stored in cookies by default, but if our Session object is too large, there are some additional problems:

  • As mentioned earlier, browsers usually have a maximum Cookie size and may refuse to save if the Session is set too large.
  • Cookies are carried with each request, and when the Session becomes too large, a large amount of additional Cookie information is carried with each request.

We just need to set app.sessionStore to store the Session to the specified store.

// app.js
module.exports = app= > {
  app.sessionStore = {
    // support promise / async
    async get (key) {
      // return value;
    },
    async set (key, value, maxAge) {
      // set key to store
    },
    async destroy (key) {
      // destroy key}}; };Copy the code

The implementation of sessionStore can also be encapsulated in plug-ins. For example, egg-session-redis provides the ability to store sessions in Redis. In the application layer, We just need to introduce the egg-redis and egg-session-redis plug-ins.

// plugin.js
exports.redis = {
  enable: true.package: 'egg-redis'}; exports.sessionRedis = {enable: true.package: 'egg-session-redis'};Copy the code

Once a Session is stored in an external store, it means that the system is heavily dependent on the external store, and when it dies, session-related functions are completely unavailable. Therefore, we recommend that you store only necessary information in the Session, keep the Session compact and use the default Cookie, and do not store user-level cache in the Session.

Practice Session

Example Change the expiration time of a user Session

Although there is an item in the Session configuration called maxAge, it can only set the duration of the Session globally. We often see the remember me option box on the login page of some websites, which can be checked to make the duration of the Session longer. This setting of the Session duration for a specific user can be implemented with ctx.session.maxAge=.

const ms = require('ms');
class UserController extends Controller {
  async login() {
    const ctx = this.ctx;
    const { username, password, rememberMe } = ctx.request.body;
    const user = await ctx.loginAndGetUser(username, password);

    / / set the Session
    ctx.session.user = user;
    // If the user checks' remember me ', set an expiration time of 30 days
    if (rememberMe) ctx.session.maxAge = ms('30d'); }}Copy the code
Extend the validity period of a user Session

By default, the framework does not extend Session validity when a user request does not result in a Session modification, but in some cases we want to extend the validity of a Session if the user has been visiting our site for an extended period of time, so that the user does not log out.

// config/config.default.js
module.exports = {
  session: {
    renew: true,}};Copy the code

Exception handling

errorPageUrl

The onError plugin is configured to support the errorPageUrl attribute. When errorPageUrl is configured, the user will be redirected to this address if the HTML page applied on the line is abnormal.

Configure the static file address in config/config.default.js

// config/config.default.js
module.exports = {
  static: {
    prefix: '/'.dir: path.join(appInfo.baseDir, 'app/public'),}};Copy the code
// config/config.default.js
module.exports = {
  onerror: {
    // When an exception occurs on an online page, redirect to this page
    errorPageUrl: '/50x.html',}};Copy the code

Custom unified exception handling

// config/config.default.js
module.exports = {
  onerror: {
    all(err, ctx) {
      // Define error handling for all response types here
      // Note that other error handling methods do not take effect after config.all is defined
      ctx.body = 'error';
      ctx.status = 500;
    },
    html(err, ctx) {
      // html hander
      ctx.body = '<h3>error</h3>';
      ctx.status = 500;
    },
    json(err, ctx) {
      // json hander
      ctx.body = { message: 'error' };
      ctx.status = 500;
    },
    jsonp(err, ctx) {
      // In general, there is no need to specify jSONP as jSONP error handling automatically calls JSON error handling, wrapped in jSONP response format,}}};Copy the code

404

The framework does not treat the 404 status returned by the server as an exception, but the framework provides a default response when the response is 404 and no body is returned.

  • When a request is determined by the framework to require a JSON-formatted response, a JSON fragment is returned:
{ "message": "Not Found" }
Copy the code
  • When a request is determined by the framework to require an HTML response, an HTML is returned:
<h1>404 Not Found</h1>
Copy the code

The framework supports configuration to redirect a 404 response to a default HTML request to a specified page.

// config/config.default.js
module.exports = {
  notfound: {
    pageUrl: '/404.html',}};Copy the code

Customize the 404 response

In some scenarios, we need to customize the server 404 response. As with custom exception handling, we also need to add a middleware to handle 404 uniformly:

// app/middleware/notfound_handler.js
module.exports = (a)= > {
  return async function notFoundHandler(ctx, next) {
    await next();
    if (ctx.status === 404 && !ctx.body) {
      if (ctx.acceptJSON) {
        ctx.body = { error: 'Not Found' };
      } else {
        ctx.body = '<h1>Page Not Found</h1>'; }}}; };Copy the code

Introducing middleware into the configuration:

// config/config.default.js
module.exports = {
  middleware: [ 'notfoundHandler']};Copy the code

MySQL

egg-mysql

The framework provides the egg-mysql plug-in to access the mysql database. This plug-in can access both a normal MySQL database and an online database service based on the MySQL protocol

Installation and Configuration

npm i --save egg-mysql
Copy the code

Enable plug-in:

// config/plugin.js
exports.mysql = {
  enable: true.package: 'egg-mysql'};Copy the code

Configure database connection information for each environment in config/config.${env}.js.

A single data source

If our application needs to access only one instance of the MySQL database, it can be configured as follows:

// config/config.${env}.js
exports.mysql = {
  // Configure single database information
  client: {
    // host
    host: 'mysql.com'./ / the port number
    port: '3306'./ / user name
    user: 'test_user'./ / password
    password: 'test_password'.// Database name
    database: 'test',},// Whether to load to app, enabled by default
  app: true.// Whether to load it to the agent
  agent: false};Copy the code

Usage:

await app.mysql.query(sql, values); // Single instances can be accessed directly through app.mysql
Copy the code

Multiple data sources

exports.mysql = {
  clients: {
    Get ('clientId') to obtain the client instance
    db1: {
      // host
      host: 'mysql.com'./ / the port number
      port: '3306'./ / user name
      user: 'test_user'./ / password
      password: 'test_password'.// Database name
      database: 'test',},db2: {
      // host
      host: 'mysql2.com'./ / the port number
      port: '3307'./ / user name
      user: 'test_user'./ / password
      password: 'test_password'.// Database name
      database: 'test',},// ...
  },
  // Default values for all database configurations
  default: {},// Whether to load to app, enabled by default
  app: true.// Whether to load it to the agent
  agent: false};Copy the code

Usage:

const client1 = app.mysql.get('db1');
await client1.query(sql, values);

const client2 = app.mysql.get('db2');
await client2.query(sql, values);
Copy the code

The Service layer

Since access to the MySQL database is in the data processing layer of the Web layer, we strongly recommend that this part of the code be maintained in the Service layer.

// app/service/user.js
class UserService extends Service {
  async find(uid) {
    // Get user details from the database
    const user = await this.app.mysql.get('users', { id: 11 });
    return{ user }; }}// app/controller/user.js
class UserController extends Controller {
  async info() {
    const ctx = this.ctx;
    const userId = ctx.params.id;
    const user = awaitctx.service.user.find(userId); ctx.body = user; }}Copy the code

How do I write CRUD statements

Create

/ / insert
const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // Insert the record with title Hello World in the POST table

=> INSERT INTO `posts`(`title`) VALUES('Hello World');

console.log(result); = > {fieldCount: 0.affectedRows: 1.insertId: 3710.serverStatus: 2.warningCount: 2.message: ' '.protocol41: true.changedRows: 0
}

// Check whether the insertion is successful
const insertSuccess = result.affectedRows === 1;
Copy the code

Read

You can retrieve one or more records directly using the GET or SELECT methods. The SELECT method supports conditional query and result customization.

const post = await this.app.mysql.get('posts', { id: 12 });

=> SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0.1;
Copy the code
Query the full table
const results = await this.app.mysql.select('posts');

=> SELECT * FROM `posts`;
Copy the code
Conditional query and result customization
  • whereQuery conditions{ status: 'draft', author: ['author1', 'author2'] }
  • columnsThe column name of the query['author', 'title']
  • ordersThe sorting way[['created_at','desc'], ['id','desc']]
  • limit 10Query the number
  • offset 0The offset
const results = await this.app.mysql.select('posts', { // Search for the POST table
  where: { status: 'draft'.author: ['author1'.'author2']},/ / the WHERE condition
  columns: ['author'.'title'].// The table field to query
  orders: [['created_at'.'desc'], ['id'.'desc']], // Sort
  limit: 10.// Return the amount of data
  offset: 0.// Data offset
});

=> SELECT `author`.`title` FROM `posts`
  WHERE `status` = 'draft' AND `author` IN('author1'.'author2')
  ORDER BY `created_at` DESC, `id` DESC LIMIT 0.10;
Copy the code

Update

// Modify the data, will be based on the primary key ID, and update
const row = {
  id: 123.name: 'fengmk2'.otherField: 'other field value'.// any other fields u want to update
  modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};
const result = await this.app.mysql.update('posts', row); // Update the records in the posts table

=> UPDATE `posts` SET `name` = 'fengmk2'.`modifiedAt` = NOW() WHERE id = 123 ;

// Check that the update is successful
const updateSuccess = result.affectedRows === 1;


// If the primary key is a custom ID name, such as custom_id, it needs to be set in 'where'
const row = {
  name: 'fengmk2'.otherField: 'other field value'.// any other fields u want to update
  modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};

const options = {
  where: {
    custom_id: 456}};const result = await this.app.mysql.update('posts', row, options); // Update the records in the posts table

=> UPDATE `posts` SET `name` = 'fengmk2'.`modifiedAt` = NOW() WHERE custom_id = 456 ;

// Check that the update is successful
const updateSuccess = result.affectedRows === 1;
Copy the code

Delete

const result = await this.app.mysql.delete('posts', {
  author: 'fengmk2'}); => DELETE FROM`posts` WHERE `author` = 'fengmk2';
Copy the code

Execute SQL statements directly

You can use Query to execute legitimate SQL statements.

We strongly discourage developers from concatenating SQL statements, as this can easily cause SQL injection!!

If you must concatenate the SQL statement yourself, use the mysql.escape method.

const postId = 1;
const results = await this.app.mysql.query('update posts set hits = (hits + ?) where id = ? '[1, postId]);

=> update posts set hits = (hits + 1) where id = 1;
Copy the code

With a transaction

Generally speaking, transactions must meet 4 conditions: Atomicity, Consistency, Isolation, reliability.

  • Atomicity: Ensures that all operations within a transaction complete successfully, otherwise the transaction is aborted at the point of failure and previous operations are rolled back to their previous state.
  • Consistency: Changes to the database are consistent.
  • Isolation: Transactions are independent of each other and do not affect each other
  • Persistence: Ensures that after a transaction is committed, the results of the transaction are permanent.

Therefore, a transaction must be accompanied by beginTransaction, COMMIT, or rollback, which respectively represent the start of the transaction and the rollback of success and failure.

Manual control

  • Advantages:beginTransaction.commitrollbackThey are completely controlled by the developer and can be very fine-grained.
  • Disadvantages: handwritten code is more, not everyone can write well. Forgetting to catch exceptions and cleanup can lead to serious bugs.
const conn = await app.mysql.beginTransaction(); // Initialize the transaction

try {
  await conn.insert(table, row1);  // First step
  await conn.update(table, row2);  // Step 2
  await conn.commit(); // Commit the transaction
} catch (err) {
  // error, rollback
  await conn.rollback(); // Roll back the transaction after catching the exception!!
  throw err;
}
Copy the code

Automatic control: Transaction with Scope

  • API:beginTransactionScope(scope, ctx)
    • scope: a generatorFunction in which all SQL statements for the transaction are executed.
    • ctx: the context object of the current request. Passing in CTX ensures that there is only one active transaction at a time in a request, even in the case of transaction nesting.
const result = await app.mysql.beginTransactionScope(async conn => {
  // don't commit or rollback by yourself
  await conn.insert(table, row1);
  await conn.update(table, row2);
  return { success: true };
}, ctx); // CTX is the context of the current request, which can be obtained from 'this.ctx' if it is in a service file
// if error throw on scope, will auto rollback
Copy the code

Expressions (Literal)

If you need to call MySQL’s built-in functions (or expressions), you can use Literal.

Built-in expression

  • NOW(): Indicates the current system time of the databaseapp.mysql.literals.nowTo obtain.
await this.app.mysql.insert(table, {
  create_time: this.app.mysql.literals.now,
});

=> INSERT INTO `$table`(`create_time`) VALUES(NOW())
Copy the code

Custom expression

The following example shows how to call the MySQL built-in CONCAT(s1,… Sn) function, do string concatenation.

const Literal = this.app.mysql.literals.Literal;
const first = 'James';
const last = 'Bond';
await this.app.mysql.insert(table, {
  id: 123.fullname: new Literal(`CONCAT("${first}","${last}"`)}); => INSERT INTO`$table`(`id`.`fullname`) VALUES(123, CONCAT("James"."Bond"))
Copy the code

Printing query Logs

When it starts up

windows

set DEBUG=ali-rds* && npm run dev
Copy the code

Linux, MAC

DEBUG=ali-rds* npm run dev
Copy the code

Sequelize

In the Node.js community, Sequelize is a widely used ORM framework that supports multiple data sources such as MySQL, PostgreSQL, SQLite, and MSSQL.

Initialize the project

Install dependencies

npm install --save egg-sequelize mysql2
Copy the code

Introduce the egg-sequelize plug-in in config/plugin.js

exports.sequelize = {
  enable: true.package: 'egg-sequelize'};Copy the code

Write the sequelize configuration in config/config.default.js

config.sequelize = {
  dialect: 'mysql'.host: '127.0.0.1'.port: 3306.database: 'egg-sequelize-doc-default'};Copy the code

We can configure different data sources in different environment configuration address, used to distinguish between different environment using the database, for example, we can create a new config/config. The unittest. Js configuration file, write the following configuration, Point the database connected in single test to egg-sequelize-doc-unittest.

exports.sequelize = {
  dialect: 'mysql'.host: '127.0.0.1'.port: 3306.database: 'egg-sequelize-doc-unittest'};Copy the code

Initialize the database and Migrations

How do you track data changes from iteration to iteration and quickly change data structures between different environments (development, test, CI) and iterations as the project evolves? This is where Migrations are needed to help us manage changes to our data structures.

Sequelize provides the sequelize-CLI tool to implement Migrations, and we can also introduce sequelize-CLI in egg projects.

Install sequelize – cli

npm install --save-dev sequelize-cli
Copy the code

In the Egg project, we want to put all migrations-related content in the Database directory, so we create a new.Sequelizer configuration file in the project root directory:

'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),};Copy the code

Initializes Migrations configuration files and directories

npx sequelize init:config
npx sequelize init:migrations
Copy the code

Json file and database/migrations directory will be generated after execution. We will modify the contents of database/config.json to the database configuration used in our project:

{
  "development": {
    "username": "root"."password": null."database": "egg-sequelize-doc-default"."host": "127.0.0.1"."dialect": "mysql"
  },
  "test": {
    "username": "root"."password": null."database": "egg-sequelize-doc-unittest"."host": "127.0.0.1"."dialect": "mysql"}}Copy the code

With sequelize-CLI and associated configuration initialized, we can start writing the project’s first Migration file to create one of our Users tables.

npx sequelize migration:generate --name=init-users
Copy the code

The migration file (${timestamp}-init-users.js) is generated in the database/migrations directory, and we modify it to handle initializing the Users table:

'use strict';

module.exports = {
  // The function that is called when performing the database upgrade to create the Users table
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING } = Sequelize;
    await queryInterface.createTable('users', {
      id: { type: INTEGER, primaryKey: true.autoIncrement: true },
      name: STRING(30),
      age: INTEGER,
      created_at: DATE,
      updated_at: DATE,
    });
  },
  // Drop the Users table from the function called when the database is degraded
  down: async queryInterface => {
    await queryInterface.dropTable('users'); }};Copy the code
Update database
npx sequelize db:migrate
If you have problems and need to roll back a change, use 'db:migrate:undo' to roll back a change
# npx sequelize db:migrate:undo
You can use 'db:migrate:undo:all' to revert to the initial state
# npx sequelize db:migrate:undo:all
Copy the code

Write the code

Let’s start by writing the user model in app/model/ :

'use strict';

module.exports = app= > {
  const { STRING, INTEGER, DATE } = app.Sequelize;

  const User = app.model.define('user', {
    id: { type: INTEGER, primaryKey: true.autoIncrement: true },
    name: STRING(30),
    age: INTEGER,
    created_at: DATE,
    updated_at: DATE,
  });

  return User;
};
Copy the code

This Model can be accessed in Controller and Service via app.model.User or ctx.model.User. For example, we write app/ Controller /users.js:

// app/controller/users.js
const Controller = require('egg').Controller;

function toInt(str) {
  if (typeof str === 'number') return str;
  if(! str)return str;
  return parseInt(str, 10) | |0;
}

class UserController extends Controller {
  async index() {
    const ctx = this.ctx;
    const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
    ctx.body = await ctx.model.User.findAll(query);
  }

  async show() {
    const ctx = this.ctx;
    ctx.body = await ctx.model.User.findByPk(toInt(ctx.params.id));
  }

  async create() {
    const ctx = this.ctx;
    const { name, age } = ctx.request.body;
    const user = await ctx.model.User.create({ name, age });
    ctx.status = 201;
    ctx.body = user;
  }

  async update() {
    const ctx = this.ctx;
    const id = toInt(ctx.params.id);
    const user = await ctx.model.User.findByPk(id);
    if(! user) { ctx.status =404;
      return;
    }

    const { name, age } = ctx.request.body;
    await user.update({ name, age });
    ctx.body = user;
  }

  async destroy() {
    const ctx = this.ctx;
    const id = toInt(ctx.params.id);
    const user = await ctx.model.User.findByPk(id);
    if(! user) { ctx.status =404;
      return;
    }

    await user.destroy();
    ctx.status = 200; }}module.exports = UserController;
Copy the code

Tool to summarize

  • sequelize-cli
  • Sequelize-auto Automatically exports bare Sequelize Models from the database