1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Webの勉強はじめてみた その27〜モジュールごとの実装とデータベース〜

Posted at

N予備校「プログラミング入門Webアプリ」を受講しています。
今回は第3章20節〜26節です。
承認されたユーザーだけが使える匿名掲示板の作成。
気をつけたい箇所や気付いた点だけをまとめました。

設計の進め方

1. システム要件の定義 2. UIの設計: ページに何をどう配置するか 3. URIの設計: パスの作り方(RESTful) 4. モジュールの設計

URIとモジュール

:::note リクエストを具体的な処理に振り分けることをルーティング、 リクエストに対し具体的な処理をする関数を リクエストハンドラという :::

サーバーを起動するもの
リクエストを処理するもの
ルーティングを行うもの

など、それぞれ役割を決めて実装する。
メインとなる機能から実装していくのが良い。
処理を実装する前にそれぞれの機能などをコメントする。

覚えておきたいこと

:::note テンプレートエンジンなどを使う場合、のちにhtmlになるものはviewsディレクトリに格納する。 :::

以下はPOSTされた時の処理

// POSTの処理
      let body =[];
      req.on('data', (chunk) => {
        body.push(chunk);
      }).on('end', ()=>{
        //  クエリから値を取得
        body = Buffer.concat(body).toString();
        const params = new URLSearchParams(body);
        const content = params.get('content');
        console.info(`投稿されました: ${content}`);                
      });

querystringが非推奨になったのでURLSearchParamを使う。URIエンコードされた文字列のデコードも同時に行うので、decodeURIComponentはいらない。

認証機能

`http-auth`モジュールの、ファイルを利用した認証
users.htpasswd
admin:apple
guest1:1234
guest2:5678
const basic = auth.basic({
  realm: 'Enter username and password.',
  file: './users.htpasswd'
})

Basic認証では、特定のURLにアクセスした際、ステータスコード 401 - Unauthorized を返すことでログアウトされる。

function handleLogout(req, res){
  handleStatus(req, res, 401, 'ログアウトしました');  
}

function handleNotFound(req, res){
  handleStatus(req, res, 404, 'ページが見つかりません');  
}

function handleBadRequest(req, res){
  handleStatus(req, res, 400, '未対応のメソッドです');
}
/**
 * HTTPレスポンスの共通処理
 * @param {Object} req HTTP Request
 * @param {Object} res HTTP Response
 * @param {Number} statusCode 
 * @param {String} msg Message
 */
function handleStatus(req, res, statusCode, msg){
  res.writeHead(statusCode, {
    'Content-Type' : 'text/plain; charset=utf-8'
  });
  res.end(msg);
}

データベース

今回はPostgreSQLを`sequelize`モジュールで操作。
docker-compose.yml
services:
  app:
    depends_on:
        - db
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: secret_board
      TZ: "Asia/Tokyo"

docker-compose.ymldbというサービス名でPostgreSQLのコンテナを追加。
仮想環境上でappdbの二つのサーバーが稼働する状態。

Sequelize

yarn add sequelize@6.5.0
yarn add pg@8.5.1
yarn add pg-hstore@2.3.3

データベースの構成を設定することを、データモデリングという

post.js
// sequelizeの基本設定
const { Sequelize, DataTypes } = require('sequelize');
// データベース全体の設定
const sequelize = new Sequelize(
  // IDとパスワードを渡す
  'postgres://postgres:postgres@db/secret_board',
  {
    // 起動ログなどのログを出力しない
    logging: false
  }
);
// データモデリング
const Post = sequelize.define(
  // テーブル名
  'Post',
  {
    // データ定義
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    content: {
      type: DataTypes.TEXT
    },
    postedBy: {
      type: DataTypes.STRING
    },
    trackingCookie: {
      type: DataTypes.STRING
    }
  },
  {
    // テーブル名の固定
    freezeTableName: true,
    // createdAt, updatedAtを自動追加
    timestamps: true
  }
);
// このファイルの起動時にデータベースの設定を同期する
Post.sync();
// データモデリングしたオブジェクトをモジュールとして公開
module.exports = Post;

insert

```javascript const Post = require('./post'); Post.create({ //登録したいデータ }).then(() => { //登録後に実行してほしい処理 }) ``` 非同期処理となる(Promiseオブジェクトが返ってくる)ので、thenメソッドを利用する。

コンソール上でのデータ確認

dbコンテナへ入る
docker-compose exec db bash

su postgres : ユーザーの変更(今回はpostgresというユーザー)
psql : Postgres用コンソールに入る
¥c secret_borad : データベースに接続(今回はsecret_boardというデータベース)
¥q : 終了

select

データの全件取得
// データの全件取得
Post.findAll({order:[['id', 'DESC']]}).then((posts) => {
    res.end(
      pug.renderFile('./views/posts.pug', { posts })
      );
  });

renderFileの引数に変数を指定することで、pugで参照可能となる。

delete

`destroy`で指定したキーのデータを削除 投稿者自身か管理者のみが削除できるようにする
function handleDelete(req, res){
  switch(req.method){
    case 'POST':
      let body = [];
      req.on('data', (chunk) => {
        body.push(chunk);
      }).on('end', () =>{
        body = Buffer.concat(body).toString();
        const params = new URLSearchParams(body);
        const id = params.get('id');
        Post.findByPk(id).then((post) => {
          if(req.user === post.postedBy || req.user === 'admin'){
            post.destroy().then(() => {
              handleRedirectPosts(req, res);    
            })
          } 
        })
      })
      break;
    default:
      util.handleBadRequest(req, res);
      break;
  }
}

curlなどで第三者がdeleteを送る可能性があるため、サーバー側でもユーザーのチェックをすること。

実行するとデータベースから実際にデータを削除することを物理削除、実際にデータを削除する代わりに「削除された」ことを表すフラグを立ててデータが削除されたとみなすことを論理削除という。

削除処理は慎重に。実用的にも論理削除が良い。

データベースの永続化

dockerを破棄するとデータも破棄されるので、PC側の別の場所にファイルを保存する。
docker-compose.yml
db:
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: secret_board
      TZ: "Asia/Tokyo"
    volumes:
      - ../secret-board-db:/var/lib/postgresql/data

PC側: ../secret-board-db
docker側: var/lib/postgresql/data

docker起動時に同期させる

トラッキングCookie

:::note ユーザーの行動を追跡するために付与される Cookie のことをトラッキング Cookie と呼ぶ。 :::

cookiesというnpmモジュールを利用する

yarn add cookies@0.8.0
const Cookies = require('cookies');
const trackingIdKey = 'tracking_id';
// TrackingCookie
const cookies = new Cookies(req, res);
addTrackingCookie(cookies);

function addTrackingCookie(cookies){
  // Cookieが存在しない場合
  if(!cookies.get(trackingIdKey)){
    // ランダムな数値を登録
    const trackingId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
    // 有効期限を翌日(24時間後)まで
    const tomorrow = new Date(Date.now() + (1000 * 60 * 60 * 24));
    cookies.set(trackingIdKey, trackingId, {expires: tomorrow});
  }
}

MAX_SAFE_INTEGER : JavaScriptで扱える最大値

PUG側

```pug each post in posts - let isPostedAdmin = (post.postedBy === 'admin') if isPostedAdmin h3 #{post.id} : 管理人 ★ else h3 #{post.id} : ID:#{post.trackingCookie} p!= post.content p 投稿日時: #{post.createdAt} - let isAdmin = (user === 'admin') if isAdmin p 投稿者: #{post.postedBy} - let isDeletable = (user === post.postedBy || isAdmin) if isDeletable form(method="post" action="/posts?delete=1") input(type="hidden" name="id" value=post.id) button(type="submit") 削除 hr ```

each in 
渡されてきたコレクションをループ。
-
pugテンプレート内にJavaScriptを直接記述することができる。
p!= post.content
pugテンプレート内で文字列に含まれたタグを認識させる。

まとめ

モジュールごとの実装、わかりにくかったのは名前のせいもあるのかな。 `form`からの`POST`メソッド、データベースの`Post`、データベースモデリングの`post.js`、URIクエリの`posts`。 データベースからの削除は、なるほどと思った。 実際に削除するとなるとほかのテーブルも意識しないといけないので、実務的にも慎重になる事案。 しかし設計の重要性というのをつくづく感じる今日この頃。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?