一文看懂 Eggjs-基礎全面講解(完結)

對上篇文章回顧下,上篇講到了javascript

Cookie 與 Session

Cookie

經過 ctx.cookies,咱們能夠在 controller 中便捷、安全的設置和讀取 Cookie。html

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;
  }
}
複製代碼

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

設置 Cookie 實際上是經過在 HTTP 響應中設置 set-cookie 頭完成的,每個 set-cookie 都會讓瀏覽器在 Cookie 中存一個鍵值對。在設置 Cookie 值的同時,協議還支持許多參數來配置這個 Cookie 的傳輸、存儲和權限。前端

  • {Number} maxAge: 設置這個鍵值對在瀏覽器的最長保存時間。是一個從服務器當前時刻開始的毫秒數。
  • {Date} expires: 設置這個鍵值對的失效時間,若是設置了 maxAge,expires 將會被覆蓋。若是 maxAge 和 expires 都沒設置,Cookie 將會在瀏覽器的會話失效(通常是關閉瀏覽器時)的時候失效。
  • {String} path: 設置鍵值對生效的 URL 路徑,默認設置在根路徑上(/),也就是當前域名下的全部 URL 均可以訪問這個 Cookie。
  • {String} domain: 設置鍵值對生效的域名,默認沒有配置,能夠配置成只在指定域名才能訪問。
  • {Boolean} httpOnly: 設置鍵值對是否能夠被 js 訪問,默認爲 true,不容許被 js 訪問。
  • {Boolean} secure: 設置鍵值對只在 HTTPS 鏈接上傳輸,框架會幫咱們判斷當前是否在 HTTPS 鏈接上自動設置 secure 的值。

除了這些屬性以外,框架另外擴展了 3 個參數的支持:java

  • {Boolean} overwrite:設置 key 相同的鍵值對如何處理,若是設置爲 true,則後設置的值會覆蓋前面設置的,不然將會發送兩個 set-cookie 響應頭。
  • {Boolean} signed:設置是否對 Cookie 進行簽名,若是設置爲 true,則設置鍵值對的時候會同時對這個鍵值對的值進行簽名,後面取的時候作校驗,能夠防止前端對這個值進行篡改。默認爲 true。
  • {Boolean} encrypt:設置是否對 Cookie 進行加密,若是設置爲 true,則在發送 Cookie 前會對這個鍵值對的值進行加密,客戶端沒法讀取到 Cookie 的明文值。默認爲 false。

Cookie 是加簽不加密的,瀏覽器能夠看到明文,js 不能訪問,不能被客戶端(手工)篡改。mysql

若是想要 Cookie 在瀏覽器端能夠被 js 訪問並修改:linux

ctx.cookies.set(key, value, {
  httpOnly: false,
  signed: false,
});
複製代碼
  • 因爲瀏覽器和其餘客戶端實現的不肯定性,爲了保證 Cookie 能夠寫入成功,建議 value 經過 base64 編碼或者其餘形式 encode 以後再寫入。
  • 因爲瀏覽器對 Cookie 有長度限制限制,因此儘可能不要設置太長的 Cookie。通常來講不要超過 4093 bytes。當設置的 Cookie value 大於這個值時,框架會打印一條警告日誌。

ctx.cookies.get(key, options)

上面在設置 Cookie 的時候,咱們能夠設置 options.signed 和 options.encrypt 來對 Cookie 進行簽名或加密,所以對應的在獲取 Cookie 的時候也要傳相匹配的選項。git

  • 若是設置的時候指定爲 signed,獲取時未指定,則不會在獲取時對取到的值作驗籤,致使可能被客戶端篡改。
  • 若是設置的時候指定爲 encrypt,獲取時未指定,則沒法獲取到真實的值,而是加密事後的密文。

Cookie 祕鑰

因爲咱們在 Cookie 中須要用到加解密和驗籤,因此須要配置一個祕鑰供加密使用。在 config/config.default.jsgithub

module.exports = {
  keys: 'key1,key2',
};
複製代碼

keys 配置成一個字符串,能夠按照逗號分隔配置多個 key。Cookie 在使用這個配置進行加解密時:redis

  • 加密和加簽時只會使用第一個祕鑰。
  • 解密和驗籤時會遍歷 keys 進行解密。

若是咱們想要更新 Cookie 的祕鑰,可是又不但願以前設置到用戶瀏覽器上的 Cookie 失效,能夠將新的祕鑰配置到 keys 最前面,等過一段時間以後再刪去不須要的祕鑰便可。sql

Session

Cookie 在 Web 應用中常常承擔標識請求方身份的功能,因此 Web 應用在 Cookie 的基礎上封裝了 Session 的概念,專門用作用戶身份識別。

框架內置了 Session 插件,給咱們提供了 ctx.session 來訪問或者修改當前用戶 Session 。

class HomeController extends Controller {
  async fetchPosts() {
    const ctx = this.ctx;
    // 獲取 Session 上的內容
    const userId = ctx.session.userId;
    const posts = await ctx.service.post.fetch(userId);
    // 修改 Session 的值
    ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
    ctx.body = {
      success: true,
      posts,
    };
  }
}
複製代碼

Session 的使用方法很是直觀,直接讀取它或者修改它就能夠了,若是要刪除它,直接將它賦值爲 null:

ctx.session = null;
複製代碼

須要 特別注意 的是:設置 session 屬性時須要避免如下幾種狀況(會形成字段丟失,詳見 koa-session 源碼)

  • 不要以 _ 開頭
  • 不能爲 isNew
// ❌ 錯誤的用法
ctx.session._visited = 1;   // --> 該字段會在下一次請求時丟失
ctx.session.isNew = 'HeHe'; // --> 爲內部關鍵字, 不該該去更改

// ✔️ 正確的用法
ctx.session.visited = 1;    // --> 此處沒有問題
複製代碼

Session 的實現是基於 Cookie 的,默認配置下,用戶 Session 的內容加密後直接存儲在 Cookie 中的一個字段中,用戶每次請求咱們網站的時候都會帶上這個 Cookie,咱們在服務端解密後使用。Session 的默認配置以下:

exports.session = {
  key: 'EGG_SESS',
  maxAge: 24 * 3600 * 1000, // 1 天
  httpOnly: true,
  encrypt: true,
};
複製代碼

能夠看到這些參數除了 key 都是 Cookie 的參數,key 表明了存儲 Session 的 Cookie 鍵值對的 key 是什麼。在默認的配置下,存放 Session 的 Cookie 將會加密存儲、不可被前端 js 訪問,這樣能夠保證用戶的 Session 是安全的。

擴展存儲

Session 默認存放在 Cookie 中,可是若是咱們的 Session 對象過於龐大,就會帶來一些額外的問題:

  • 前面提到,瀏覽器一般都有限制最大的 Cookie 長度,當設置的 Session 過大時,瀏覽器可能拒絕保存。
  • Cookie 在每次請求時都會帶上,當 Session 過大時,每次請求都要額外帶上龐大的 Cookie 信息。

咱們只須要設置 app.sessionStore 便可將 Session 存儲到指定的存儲中。

// 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
    },
  };
};
複製代碼

sessionStore 的實現咱們也能夠封裝到插件中,例如 egg-session-redis 就提供了將 Session 存儲到 redis 中的能力,在應用層,咱們只須要引入 egg-redisegg-session-redis 插件便可。

// plugin.js
exports.redis = {
  enable: true,
  package: 'egg-redis',
};
exports.sessionRedis = {
  enable: true,
  package: 'egg-session-redis',
};
複製代碼

一旦選擇了將 Session 存入到外部存儲中,就意味着系統將強依賴於這個外部存儲,當它掛了的時候,咱們就徹底沒法使用 Session 相關的功能了。所以咱們更推薦你們只將必要的信息存儲在 Session 中,保持 Session 的精簡併使用默認的 Cookie 存儲,用戶級別的緩存不要存儲在 Session 中。

Session 實踐

修改用戶 Session 失效時間

雖然在 Session 的配置中有一項是 maxAge,可是它只能全局設置 Session 的有效期,咱們常常能夠在一些網站的登錄頁上看到有 記住我 的選項框,勾選以後可讓登錄用戶的 Session 有效期更長。這種針對特定用戶的 Session 有效時間設置咱們能夠經過 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);

    // 設置 Session
    ctx.session.user = user;
    // 若是用戶勾選了 `記住我`,設置 30 天的過時時間
    if (rememberMe) ctx.session.maxAge = ms('30d');
  }
}
複製代碼
延長用戶 Session 有效期

默認狀況下,當用戶請求沒有致使 Session 被修改時,框架都不會延長 Session 的有效期,可是在有些場景下,咱們但願用戶若是長時間都在訪問咱們的站點,則延長他們的 Session 有效期,不讓用戶退出登陸態。

// config/config.default.js
module.exports = {
  session: {
    renew: true,
  },
};
複製代碼

異常處理

errorPageUrl

onerror 插件的配置中支持 errorPageUrl 屬性,當配置了 errorPageUrl 時,一旦用戶請求線上應用的 HTML 頁面異常,就會重定向到這個地址。

config/config.default.js 中 先配置靜態文件地址

// config/config.default.js
module.exports = {
  static: {
    prefix: '/',
      dir: path.join(appInfo.baseDir, 'app/public'),
  },
};
複製代碼
// config/config.default.js
module.exports = {
  onerror: {
    // 線上頁面發生異常時,重定向到這個頁面上
    errorPageUrl: '/50x.html',
  },
};
複製代碼

自定義統一異常處理

// config/config.default.js
module.exports = {
  onerror: {
    all(err, ctx) {
      // 在此處定義針對全部響應類型的錯誤處理方法
      // 注意,定義了 config.all 以後,其餘錯誤處理方法不會再生效
      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) {
      // 通常來講,不須要特殊針對 jsonp 進行錯誤定義,jsonp 的錯誤處理會自動調用 json 錯誤處理,幷包裝成 jsonp 的響應格式
    },
  },
};
複製代碼

404

框架並不會將服務端返回的 404 狀態當作異常來處理,可是框架提供了當響應爲 404 且沒有返回 body 時的默認響應。

  • 當請求被框架斷定爲須要 JSON 格式的響應時,會返回一段 JSON:
{ "message": "Not Found" }
複製代碼
  • 當請求被框架斷定爲須要 HTML 格式的響應時,會返回一段 HTML:
<h1>404 Not Found</h1>
複製代碼

框架支持經過配置,將默認的 HTML 請求的 404 響應重定向到指定的頁面。

// config/config.default.js
module.exports = {
  notfound: {
    pageUrl: '/404.html',
  },
};
複製代碼

自定義 404 響應

在一些場景下,咱們須要自定義服務器 404 時的響應,和自定義異常處理同樣,咱們也只須要加入一箇中間件便可對 404 作統一處理:

// app/middleware/notfound_handler.js
module.exports = () => {
  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>';
      }
    }
  };
};
複製代碼

在配置中引入中間件:

// config/config.default.js
module.exports = {
  middleware: [ 'notfoundHandler' ],
};
複製代碼

MySQL

egg-mysql

框架提供了 egg-mysql 插件來訪問 MySQL 數據庫。這個插件既能夠訪問普通的 MySQL 數據庫,也能夠訪問基於 MySQL 協議的在線數據庫服務

安裝與配置

npm i --save egg-mysql
複製代碼

開啓插件:

// config/plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};
複製代碼

config/config.${env}.js 配置各個環境的數據庫鏈接信息。

單數據源

若是咱們的應用只須要訪問一個 MySQL 數據庫實例,能夠以下配置:

// config/config.${env}.js
exports.mysql = {
  // 單數據庫信息配置
  client: {
    // host
    host: 'mysql.com',
    // 端口號
    port: '3306',
    // 用戶名
    user: 'test_user',
    // 密碼
    password: 'test_password',
    // 數據庫名
    database: 'test',
  },
  // 是否加載到 app 上,默認開啓
  app: true,
  // 是否加載到 agent 上,默認關閉
  agent: false,
};
複製代碼

使用方式:

await app.mysql.query(sql, values); // 單實例能夠直接經過 app.mysql 訪問
複製代碼

多數據源

exports.mysql = {
  clients: {
    // clientId, 獲取client實例,須要經過 app.mysql.get('clientId') 獲取
    db1: {
      // host
      host: 'mysql.com',
      // 端口號
      port: '3306',
      // 用戶名
      user: 'test_user',
      // 密碼
      password: 'test_password',
      // 數據庫名
      database: 'test',
    },
    db2: {
      // host
      host: 'mysql2.com',
      // 端口號
      port: '3307',
      // 用戶名
      user: 'test_user',
      // 密碼
      password: 'test_password',
      // 數據庫名
      database: 'test',
    },
    // ...
  },
  // 全部數據庫配置的默認值
  default: {

  },

  // 是否加載到 app 上,默認開啓
  app: true,
  // 是否加載到 agent 上,默認關閉
  agent: false,
};
複製代碼

使用方式:

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

const client2 = app.mysql.get('db2');
await client2.query(sql, values);
複製代碼

Service 層

因爲對 MySQL 數據庫的訪問操做屬於 Web 層中的數據處理層,所以咱們強烈建議將這部分代碼放在 Service 層中維護。

// app/service/user.js
class UserService extends Service {
  async find(uid) {
    // 假如 咱們拿到用戶 id 從數據庫獲取用戶詳細信息
    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 = await ctx.service.user.find(userId);
    ctx.body = user;
  }
}
複製代碼

如何編寫 CRUD 語句

Create

// 插入
const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 post 表中,插入 title 爲 Hello World 的記錄

=> 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
}

// 判斷插入成功
const insertSuccess = result.affectedRows === 1;
複製代碼

Read

能夠直接使用 get 方法或 select 方法獲取一條或多條記錄。select 方法支持條件查詢與結果的定製。

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

=> SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0, 1;
複製代碼
查詢全表
const results = await this.app.mysql.select('posts');

=> SELECT * FROM `posts`;
複製代碼
條件查詢和結果定製
  • where 查詢條件 { status: 'draft', author: ['author1', 'author2'] }
  • columns 查詢的列名 ['author', 'title']
  • orders 排序方式 [['created_at','desc'], ['id','desc']]
  • limit 10 查詢條數
  • offset 0 偏移量
const results = await this.app.mysql.select('posts', { // 搜索 post 表
  where: { status: 'draft', author: ['author1', 'author2'] }, // WHERE 條件
  columns: ['author', 'title'], // 要查詢的表字段
  orders: [['created_at','desc'], ['id','desc']], // 排序方式
  limit: 10, // 返回數據量
  offset: 0, // 數據偏移量
});

=> SELECT `author`, `title` FROM `posts`
  WHERE `status` = 'draft' AND `author` IN('author1','author2')
  ORDER BY `created_at` DESC, `id` DESC LIMIT 0, 10;
複製代碼

Update

// 修改數據,將會根據主鍵 ID 查找,並更新
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); // 更新 posts 表中的記錄

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

// 判斷更新成功
const updateSuccess = result.affectedRows === 1;


// 若是主鍵是自定義的 ID 名稱,如 custom_id,則須要在 `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); // 更新 posts 表中的記錄

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

// 判斷更新成功
const updateSuccess = result.affectedRows === 1;
複製代碼

Delete

const result = await this.app.mysql.delete('posts', {
  author: 'fengmk2',
});

=> DELETE FROM `posts` WHERE `author` = 'fengmk2';
複製代碼

直接執行 sql 語句

使用 query 能夠執行合法的 sql 語句。

咱們極其不建議開發者拼接 sql 語句,這樣很容易引發 sql 注入!!

若是必需要本身拼接 sql 語句,請使用 mysql.escape 方法。

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;
複製代碼

使用事務

通常來講,事務是必須知足4個條件(ACID): Atomicity(原子性)、Consistency(一致性)、Isolation(隔離性)、Durability(可靠性)

  • 原子性:確保事務內的全部操做都成功完成,不然事務將被停止在故障點,之前的操做將回滾到之前的狀態。
  • 一致性:對於數據庫的修改是一致的。
  • 隔離性:事務是彼此獨立的,不互相影響
  • 持久性:確保提交事務後,事務產生的結果能夠永久存在。

所以,對於一個事務來說,必定伴隨着 beginTransaction、commit 或 rollback,分別表明事務的開始,成功和失敗回滾。

手動控制

  • 優勢:beginTransaction, commitrollback 都由開發者來徹底控制,能夠作到很是細粒度的控制。
  • 缺點:手寫代碼比較多,不是每一個人都能寫好。忘記了捕獲異常和 cleanup 都會致使嚴重 bug。
const conn = await app.mysql.beginTransaction(); // 初始化事務

try {
  await conn.insert(table, row1);  // 第一步操做
  await conn.update(table, row2);  // 第二步操做
  await conn.commit(); // 提交事務
} catch (err) {
  // error, rollback
  await conn.rollback(); // 必定記得捕獲異常後回滾事務!!
  throw err;
}
複製代碼

自動控制:Transaction with scope

  • API:beginTransactionScope(scope, ctx)
    • scope: 一個 generatorFunction,在這個函數裏面執行此次事務的全部 sql 語句。
    • ctx: 當前請求的上下文對象,傳入 ctx 能夠保證即使在出現事務嵌套的狀況下,一次請求中同時只有一個激活狀態的事務。
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 是當前請求的上下文,若是是在 service 文件中,能夠從 `this.ctx` 獲取到
// if error throw on scope, will auto rollback
複製代碼

表達式(Literal)

若是須要調用 MySQL 內置的函數(或表達式),可使用 Literal

內置表達式

  • NOW():數據庫當前系統時間,經過 app.mysql.literals.now 獲取。
await this.app.mysql.insert(table, {
  create_time: this.app.mysql.literals.now,
});

=> INSERT INTO `$table`(`create_time`) VALUES(NOW())
複製代碼

自定義表達式

下例展現瞭如何調用 MySQL 內置的 CONCAT(s1, ...sn) 函數,作字符串拼接。

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"))
複製代碼

打印查詢日誌

啓動的時候

windows

set DEBUG=ali-rds* && npm run dev
複製代碼

linux、 mac

DEBUG=ali-rds* npm run dev
複製代碼

Sequelize

在 Node.js 社區中,sequelize 是一個普遍使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多個數據源。

初始化項目

安裝依賴

npm install --save egg-sequelize mysql2
複製代碼

config/plugin.js 中引入 egg-sequelize 插件

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
};
複製代碼

config/config.default.js 中編寫 sequelize 配置

config.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'egg-sequelize-doc-default',
};
複製代碼

咱們能夠在不一樣的環境配置中配置不一樣的數據源地址,用於區分不一樣環境使用的數據庫,例如咱們能夠新建一個 config/config.unittest.js 配置文件,寫入以下配置,將單測時鏈接的數據庫指向 egg-sequelize-doc-unittest。

exports.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'egg-sequelize-doc-unittest',
};
複製代碼

初始化數據庫和 Migrations

在項目的演進過程當中,每個迭代都有可能對數據庫數據結構作變動,怎樣跟蹤每個迭代的數據變動,並在不一樣的環境(開發、測試、CI)和迭代切換中,快速變動數據結構呢?這時候咱們就須要 Migrations 來幫咱們管理數據結構的變動了。

sequelize 提供了 sequelize-cli 工具來實現 Migrations,咱們也能夠在 egg 項目中引入 sequelize-cli。

安裝 sequelize-cli

npm install --save-dev sequelize-cli
複製代碼

在 egg 項目中,咱們但願將全部數據庫 Migrations 相關的內容都放在 database 目錄下,因此咱們在項目根目錄下新建一個 .sequelizer 配置文件:

'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'),
};
複製代碼

初始化 Migrations 配置文件和目錄

npx sequelize init:config
npx sequelize init:migrations
複製代碼

執行完後會生成 database/config.json 文件和 database/migrations 目錄,咱們修改一下 database/config.json 中的內容,將其改爲咱們項目中使用的數據庫配置:

{
  "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"
  }
}
複製代碼

時 sequelize-cli 和相關的配置也都初始化好了,咱們能夠開始編寫項目的第一個 Migration 文件來建立咱們的一個 users 表了。

npx sequelize migration:generate --name=init-users
複製代碼

執行完後會在 database/migrations 目錄下生成一個 migration 文件(${timestamp}-init-users.js),咱們修改它來處理初始化 users 表:

'use strict';

module.exports = {
  // 在執行數據庫升級時調用的函數,建立 users 表
  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,
    });
  },
  // 在執行數據庫降級時調用的函數,刪除 users 表
  down: async queryInterface => {
    await queryInterface.dropTable('users');
  },
};
複製代碼
# 升級數據庫
npx sequelize db:migrate
# 若是有問題須要回滾,能夠經過 `db:migrate:undo` 回退一個變動
# npx sequelize db:migrate:undo
# 能夠經過 `db:migrate:undo:all` 回退到初始狀態
# npx sequelize db:migrate:undo:all
複製代碼

編寫代碼

首先咱們來在 app/model/ 目錄下編寫 user 這個 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;
};
複製代碼

這個 Model 就能夠在 Controller 和 Service 中經過 app.model.User 或者 ctx.model.User 訪問到了,例如咱們編寫 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;
複製代碼

工具總結

相關文章
相關標籤/搜索