PostgreSQL
(ポスグレ)にSequelize
(シーケライズ)でデータベースを作成してExpress
でAPI
を叩けるようにTypeScript
で実装するまでのまとめです。初心者向けの記事なので、知見のある方はご配慮お願いします。
"dependencies": {
"@nuxt/typescript-runtime": "^latest",
"@nuxtjs/axios": "^latest",
"express": "^latest",
"nuxt": "^latest",
"pg": "^latest",
"pg-hstore": "^latest",
"sequelize": "^latest",
"typescript": "^latest",
"uuid": "^latest",
"body-parser": "^latest",
"multer": "^latest",
},
パッケージは事前にインストールお願い致します。ドッカーコンポーズでnpmやデータベースを起動させます。
version: 'latest'
services:
app:
image: node: latest
command: npm run dev
working_dir: /src/app
volumes:
- .:/src/app
links:
- database
ports:
- 3000:3000
environment:
-
db:
image: postgres
environment:
- POSTGRES_PASSWORD=hogehoge
volumes:
- type: volume
source: dbdata
target: /var/lib/postgresql/data
volumes:
dbdata:
まずはデータベースの作成です。Node.ts(サーバーサイドをTypeScriptで記述)のORM(sequelize)で作っていきます。
import {
Sequelize,
UUID,
UUIDV4,
INTEGER,
STRING,
BLOB,
FLOAT,
ENUM,
} from 'sequelize'
// シーケライズからインポートさせます。サジェストされる型をチョイスして下さい。
export const config = new Sequelize(
'postgres://postgres:postgres@db/postgres',
{ define: { underscored: true } }
)
// underscored: true で命名規則をアンダースコアに指定。
export const HogeData = config.define(
'hoge',
{
// hogesテーブルを作成したいので。下はカラムになります。
id: {
type: UUID,
defaultValue: UUIDV4,
primaryKey: true,
allowNull: false,
},
// 乱数で生成されるUUIDv4はdefaultValueに設定。
hoge_id: {
type: UUID,
allowNull: false,
},
// サブでuuidを生成したいので作成。
number: {
type: INTEGER,
allowNull: false,
},
// 番号で並べ替えさせたいので数値型。
name: {
type: STRING,
},
// 文字列で名前を入力。
image: {
type: BLOB,
},
// バイナリデータを格納したい時はデータサイズ指定無しのBLOB型。
x: {
type: FLOAT,
},
// 座標で使用したかったので浮動小数点型であるFLOATを使用。
y: {
type: FLOAT,
},
path: {
type: STRING,
allowNull: false,
},
// 画像や動画のファイル名。
mimetype: {
type: STRING,
allowNull: false,
},
// ファイルの形式を挿入。
hoge_status: {
type: ENUM,
values: ['none', 'hoge-on', 'hoge-off']
},
// 列挙型で3つの定数をひとまとめに。
},
{}
)
const setup = async () => {
await config.authenticate()
HogeData.sync({ alter: true })
}
// 認証ハンドラーでデータを残して置くために alter: true。
setup()
ドッカーコンポーズのexecオプションでターミナルからデータベースを覗いてみます。上記の実装で下記hogesテーブルが作成されました。
docker-compose exec db psql -U postgres
※ exec(コマンド) db(サービス名) -U(ユーザー名) postgres(実行したいデータベース)
postgres=# \d hoges;
Table "public.hoges"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+---------
id | uuid | | not null |
hoge_id | uuid | | not null |
number | integer | | not null |
name | character varying(255) | | |
image | bytea | | |
x | double precision | | |
y | double precision | | |
hoge_status | enum_hoges_hoge_status | | |
created_at | timestamp with time zone | | not null |
updated_at | timestamp with time zone | | not null |
path | character varying(255) | | not null |
mimetype | character varying(255) | | not null |
Indexes:
"hoges_pkey" PRIMARY KEY, btree (id)
何がしたいのかって言われるとただ単に使い方をまとめているのだけなので皆さんは応用してお使い下さい。
APIの実装に移る前にreq.params
req.body
res.json( )
res.send( )
などExpressのルーティング
のルート
やメソッド
のおさらいです。ルーティング
とはWEB上で情報資源を特定するのにURL
(情報資源の位置)やURN
(名前)がありますが、それらを総称しそのエンドポイントとなるURI
とクライアントアプリケーションによるリクエストに対してレスポンスで答える定義のことです。
req.params.
デフォルト(初期値){ }
の名前が付いているルート(URL)のパラメータ(値が変化する変数)にマップ(関連付けられた)されたプロパティ(フィールドなどのデータ)を含むオブジェクトのことです。
req.body.
デフォルトは未定義でリクエストをかけた場所のKEYキー
(データを定義する定数で年齢
や品種
になります)と値
(そこにセットされる変数で4歳
やお菓子
になります)のペアを含めて取得できます。
※使用するのにbody-parser
やmulter
のミドルウェアを取得して下さい。
res.json( )
引数のオブジェクトや配列をJSON.stringify()
で文字列に直してコンテンツの種類を設定してレスポンスしてくれます。
res.send( )
多種多様なレスポンスが返せるメソッドで、JSONを返す結果はres.json
と同じです。引数に配列やオブジェクトを渡した時text/json
かapplication/json
というMIMEタイプをContent-Typeに設定してくれて処理してくれます。
res.download(path [, filename] [, options] [, fn])
(path [, filename] [, options] [, fn])
内にあるfilename
(添付ファイルとしてダウンロードされるファイル名)やmimetype
(メディアタイプ)を転送してくれます。ファイルを一度ストレージのどこかに置く必要があります。
res.render( )
指定したビューファイルをブラウザに表示させます。( )
内に画面の実装ファイルを書きます。
fs.statSync() fs.readFileSync()
import fs from 'fs'
Node.jsには公式モジュールでインポートしてくれば使えます。ファイルが存在するかどうか確認する為に使用します。ファイルがない場合エラーになるのでtry
してcatch
する必要があります。同じくfs.readFileSync()
はファイルの内容を読む為に使用します。
ルーティングの[使用例]
import { Request, Response } from 'express'
import { HogeData } from '../../lib/db'
export const hogeList1 = async (_: Request, res: Response) => {
res.json(await HogeData.findAll())
}
// データベースにあるテーブルのデータを全て取得
// HogeDataの中の全ての文字列データを取得してくれます。reqを使用しない場合はアンダーバーを使用。
export const hogeList2 = async (req: Request, res: Response) => {
res.json(
await HogeData.findAll({
where: {
hoge_id: req.params.hogeId,
},
})
)
}
// データベースにあるルートIDにヒットしたデータを全て取得
// HogeDataのルートhogeIdを探して全て取得してきてくれます。
export const hogeList3 = async (req: Request, res: Response) => {
res.json(
await HogeData.findAll({
where: {
hoge_id: req.params.hogeId,
},
order: [['numbar', 'ASC',]],
})
)
}
// データベースにあるルートIDにヒットしたデータを並び順を考慮し全て取得。
// HogeDataの中のオブジェクトのhogeIdを探してnumbar(カラム)の値(フィールド)を昇順(ASC)で取得してくれます。降順にしたい場合は(DESC)。
export const hogeList4 = async (req: Request, res: Response) => {
res.json(
await HogeData.findAll({
where: {
hoge_id: req.params.hogeId,
},
attributes: { exclude: ['hoge'] },
order: [['created_at', 'DESC']],
})
)
}
// 作成された日時を後ろからhoge要素を取り除いて名前付きルートのhogeIdを探して取得してくれます。
export const hogeGet1 = async (req: Request, res: Response) => {
res.json(await HogeData.findByPk(req.params.id))
}
// findByPk(id)のidのデータのみを取得
// HogeDataの中のオブジェクトのid(uuid)を取得してくれます。
export const hogeGet2 = async (req: Request, res: Response) => {
res.json(await HogeData.findByPk(req.params.hogeId))
}
// 上のものとほとんど変わらないですが、一応。
// HogeDataの中のオブジェクトのhogeIdを取得してくれます。
export const hogeGet3 = async (req: Request, res: Response) => {
const { hogeId, hogehogeId } = req.params
res.json(
await HogeData.findOne({
where: {
id: hogehogeId,
hoge_id: hogeId,
},
attributes: { exclude: ['hoge_hoge'] },
})
)
}
// IDで特定し一致したテーブルデータを全て取得
// attributes:(属性)で配列の中身(hoge_hoge)をexclude:(除外)してます。
// 名前付きのURLルートを二種類のIDを特定して条件を満たした最初のデータを呼び出してくれます。
export const hogeGet4 = async (req: Request, res: Response) => {
const { hogeId, id } = req.params
const HogeImage = (await HogeData.findOne({
where: {
id,
hoge_id: hogeId,
},
})) as any
res.download('uploads/' + HogeImage.path, HogeImage.name)
}
// IDで特定した条件を満たすテーブルの画像データを取得
// 型でエラーが出ないようにas anyでIDを型アサーションしてコンパイルエラーを防ぎます。
// HogeDataからidとhogeIdをreq.params内から検索。
// ルート下にuploadsフォルダを作って一度保存し、HogeImage.path(filename)と
// HogeImage.name(originalname)ファイルを転送。
// 割愛しますが、hogeGet5としてHogeMovie.pathの部分だけ変更すれば同様に使えます。
export const hogeCreate1 = async (req: Request, res: Response) =>
res.json(await HogeData.create(req.body))
// クライアント側で入力されたデータをデータベースに保存
// req.bodyに作成したそれぞれのパラメータをHogeDataに保存してくれます。
export const hogeCreate2 = async (req: Request, res: Response) => {
const hoge = req.body
hoge.hoge_id = req.params.HogeId
hoge.image = fs.readFileSync((req as any).file.path)
res.json(await HogeData.create(hoge))
}
// 画像のバイナリデータをデータベースに保管するルーティング
// req.bodyをHogeDataのhogeとして、req.paramsのstring(文字列)であるIDをhoge_idに。
// 型付けをしたreq.file.pathからファイルを読み込みimageフィールドに格納。
export const hogeCreate3 = async (req: Request, res: Response) => {
const { originalname, filename, mimetype } = (req as any).file
const hogeMovie = Object.assign({}, req.body, {
hoge_id: req.params.hogeId,
name: originalname,
path: filename,
mimetype,
})
res.json(await HogeData.create(hogeMovie))
}
// 画像・動画用にデータベースに保管する為のルーティング
// vueの場合、オブジェクトに複数のプロパティを割り当てる場合Object.assign()を使用します。
// 動画をデータベースに保存するには、multerモジュールをインストールします。
// リクエスト情報であるreq.fileの中身を{ originalname, filename, mimetype }に代入し。
// データベースのhoge_idにreq.paramsからhogeIdを持ってきて。
// nameにorijinalname、pathにはfilename、mimetypeが入った配列をreq.bodyと合わせてオブジェクト化し。
// HogeDataテーブルに作成した内容を保存。
export const hogeImage = async (req: Request, res: Response) =>
res.send(((await HogeData.findByPk(req.params.hogeId)) as any).image)
// データベースのHogeDataテーブルの特定されたIDを持つ、
// imageフィールドからデータを取得。
export const hogeUpdate = async (req: Request, res: Response) => {
const { hogeId, id } = req.params
const hoge = (await HogeData.findOne({
where: {
id,
hoge_id: hogeId,
}
})) as any
res.json(await HogeData.update({ name: req.body.name }))
}
// ルート上の2つの特定のIDをを持つテーブルのnameフィールドを更新
// name以外にも変更可能
export const hogeUpdateStatus = async (req: Request, res: Response) => {
const { status } = req.body
console.log({ status, h: req.body })
res.json(await HogeData.update({
hoge_status: status,
}, {
where: {
id: req.params.hogeId
}
}))
}
// on/offなどのパラメータを渡すスイッチなどに使用
// ルート上の特定のIDに合致したstatusデータを更新させる
// hoge_statusにはクライアント側で入力された列挙型のデータが格納される。
// 一応、データが入ってきているかログで確認。
export const hogeDestroy1 = async (req: Request, res: Response) => {
const { hogeId } = req.params
res.json(
await HogeData.destroy({
where: {
id: hogeId,
},
})
)
}
// ルートの特定のIDを持ったテーブルのデータを削除
export const hogeDestroy2 = async (req: Request, res: Response) => {
const { hogeId, id } = req.params
res.json(
await HogeData.destroy({
where: {
id,
hoge_id: hogeId,
},
})
)
}
// ルート上の2つの特定のIDをを持つテーブルデータを削除
axios
を導入してAPI
を利用するのに呼ぶ為の場所が下記のapi/index.ts
になります。
import express from 'express'
import * as hoges from './hoges'
// expressとhoges/index.tsからインポート
const handler = express()
handler.use(express.json())
handler.use((req: any, res: any, next: any) => {
res.header('Access-Control-Allow-Origin', '*')
// フロントエンドのURL
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
// 他にもPATCHやOPTIONSも許可できる「
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, access_token'
)
// アプリケーションに必要なヘッダを設定
// 上記のレスポンスヘッダーをオリジンからのコードにリソースへの
// アクセスを許可するように指示するようにする
if (req.method === 'OPTIONS') {
res.send(200)
} else {
next()
}
})
const multer = require('multer')
const upload = multer({
dest: 'uploads/',
limits: { fieldSize: 100 * 1024 * 1024 },
})
// multerはExpressのミドルウェアでファイルのアップロードに必要
// axiosで呼ぶ為のhogesのコード(例)
// GET,PUT,POST,DELETEをルーティングで結ぶ
handler.get('/hoges', hoges.hogeList1)
handler.post('/hoges', hoges.hogeCreate1)
handler.get('/hoges/:hogeId', hoges.hogeGet1)
handler.delete('/hoges/:hogeId', hoges.hogeDestroy1)
handler.put('/hoges/:hogeId/hogeStatus', hoges.hogeUpdateStatus)
handler.post('/hoges/:hogeId',upload.single('image'), hoges.hogeCreate2)
handler.get('/hoges/:hogeId/image', hoges.hogeImage)
handler.get('/hoges/:hogeId/hogeImages', hoges.hogeList2)
handler.get('/hoges/:hogeId', hoges.hogeGet3)
handler.post('/hoges/:hogeId/hogeImages', upload.single('hogesImage'), hoges.hogeCreate3)
handler.get('/hoges/:hogeId/hogeMovies', hoges.hogeList4)
handler.get('/hoges/:hogeId/hogeMovies/:hogehogeId', hoges.hogeGet3)
handler.post('/hoges/:hogeId/movies', upload.single('movie'), hoges.hogeCreate3)
handler.get('/hoges/:hogeId/movies/:id', hoges.hogeCreate5)
// * 上記でも書きましたがhogeCreate5は割愛してます。
handler.delete('/hoges/:hogeId/movies/:id', hoges.hogeDestroy2)
handler.put('/hoges/:hogeId', hoges.hogeUpdate)
module.exports = {
path: '/api',
handler,
}
現在、クライアント側はVue
で実装してますがそちらでの使い方も書いてみたいと思います。