TypescriptでSequelize してみる

はじめに

前回 Sequelize の一番基本的な使い方を紹介したが、typescript で記述可能ならば、以降 ts で書きたいなと思ったので、今回は typescript の環境を紹介したいと思う。

ts で記述するメリットは、型が使えることだけではなく、エディタからの候補表示である。property や method の打ち間違いも ts ならば防げるわけだ。

目標(課題)

公式の typescript 導入紹介では、一つのファイルに Model 定義・初期化・associate・main ルーチンなどをすべて記述する。

実運用では、各 Model は models ディレクトリで管理するものだろう(sequelize-cli が初期化時に models ディレクトリを作るぐらいだ)。

なので、ただ ts 環境を作成するだけではなく、models ディレクトリで Model を管理し、main ルーチンは、main ファイルに記述する環境を構築したいと思う。

ディレクトリ構成

はじめてに最終的なディレクトリ構成や実行イメージを紹介する。

.
├── config.json          # db 接続情報
├── package.json
├── src
│   ├── main.ts          # mainルーチン
│   └── models           # Model管理
│       ├── index.ts
│       ├── project.ts   # projectモデル
│       └── task.ts      # taskモデル
└── tsconfig.json

起動方法は、ts-node を利用してnpx ts-node src/main.tsで可能とする。

インストール

Sequelize

npm -i sequelize mysql2

Typescript

npm -i -D typescript @types/node @types/validator @types/bluebird ts-node

npx typescript --initで tsconfig.json を作成して、好きな設定をしてください。

src ディレクトリに main.ts ファイルを作成して、npx ts-node src/main.tsがうまく動作することを確認できれば OK です。

model の定義

Model は、Project, Task を作成する。 それぞれの仕様は下図の通り。

f:id:poppon555:20190721132919p:plain

両 Model とも id をプライマリキーにもち、title と description に加えて createdAt, updateAt をフィールドにもつ(createdAt, updateAt は sequelize で自動で作成されるので気にしなくてよい)。

さらに Project は複数の Task をもつ関係にあり、projectId が外部キーとなっている。

これで準備が整ったので実装に入ってみる。

src/models/project.ts

import { Sequelize, Model, DataTypes } from 'sequelize';
import { HasManyCreateAssociationMixin } from 'sequelize';
import Task from './task';

export default class Project extends Model {
  public id!: number;
  public title!: string;
  public description!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  // (3) 作成したprojectをprojectIdをもつtaskを作成するメソッド
  public createTask!: HasManyCreateAssociationMixin<Task>;

  // (1)初期化
  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT }
      },
      { sequelize, tableName: 'project' }
    );
    return this;
  }

  // (2)テーブル関係を記述
  public static associate() {
    this.hasMany(Task, {
      sourceKey: 'id',
      foreignKey: 'projectId',
      constraints: false
    });
  }
}

クラスプロパティ

sequelize の Model を継承する class を作成して export させる。クラスプロパティには、定義したフィールドを記述してやる。

しかしこの記述は typescript 用であり、sequelize が利用するには、次の initialize メソッドが必要になる。

static メソッド(initialize)

このメソッドは、引数に sequelize を受け取っているように、DB 接続設定完了した Sequelize インスタンスをもとに、実際に初期化作業を行っている。

typeプロパティやtableNameプロパティなど 前回記述した js の定義と若干異なるので注意が必要である。

static メソッド(associate)

このメソッドは、Project と Task Model の関係を sequelize に知らせるためのものである。thisは Project クラス自身を指して、第一引数にTaskを受け取る。

第二引数には、関係の詳細情報を与える(任意)。

  • foreinKey
    hasMany される側(Task)の外部キーを指定する 指定しない場合、ProjectId と大文字になる
  • sourceKey
    source は、hasMany の主体(Project)側の外部キーとつながるキーを指定する。 なてくもよい。
  • constraint
    制約情報(外部キー)の有効化フラグ Project.sync({ force: true })を動作させるために false に設定。

createTask メソッド

HasManyCreateAssociationMixinという型情報があってわかりづらいが、このメソッドは Project が作成された後、それに紐づく Task を作成できるものである。

なぜ static メソッドにするのか?

Sequelize の Model クラスは、save, update, remove メソッドの結果を受け取ったときインスタンスであるが、findAll などインスタンスを検索したり、作成するときはインスタンスではなくクラスの static メソッドで行っている。

1 レコード情報をもつのが、Model インスタンスであり、テーブル全体の情報をもつのが Model クラスなわけだ。

だから、createTask のように作成された project に紐づく task を作成するなばら、通常のメソッドになり、一方テーブル定義したり、テーブル同士の関係を記述するのは、static メソッドとなる。

同じように Task モデルも作成する

src/models/task.ts

import { Sequelize, Model, DataTypes, Association } from 'sequelize';
import Project from './project';

export default class Task extends Model {
  public id!: number;
  public title!: string;
  public description!: string;
  public deadline!: Date;
  public projectId!: number;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER.UNSIGNED,
          autoIncrement: true,
          primaryKey: true
        },
        title: { type: DataTypes.STRING },
        description: { type: DataTypes.TEXT },
        deadline: { type: DataTypes.DATE },
        projectId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false }
      },
      { sequelize, tableName: 'task' }
    );
    return this;
  }

  public static associate() {
    this.belongsTo(Project, { foreignKey: 'projectId', constraints: false });
  }
}

モデルをまとめる

モデル定義が終わったので、次はこれら model をまとめる index.ts を作成しよう。

src/models/index.ts

import { Sequelize, Model } from 'sequelize';
import Project from './project';
import Task from './task';
const { database, username, password, host, dialect } = require('../../config');

// sequelizeインスタンス作成
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// (1)モデルを一つのオブジェクトにまとめる
const db = {
  Task: Task.initialize(sequelize),
  Project: Project.initialize(sequelize)
};

// (2)テーブル同士の関係を作成する
Object.keys(db).forEach(tableName => {
  const model = db[tableName];
  if (model.associate) {
    model.associate();
  }
});

export default db;

(1)複数のモデルをひとつのオブジェクトにまとめる

dbというオブジェクトに作成した Project, Task を一括管理させる。sequelize インスタンスを渡して、初期化された Project, Task モデルを管理する。

(2)テーブルの関係づけ

static メソッドの associate をもつならば、それを呼び出してテーブル同士を対応づけている。

main

ここまで来たらあとは簡単ですね。 typescript の恩恵を受けれるので打ち間違いもないだろうし。

src/main.ts

import models from './models';

(async () => {
  // Project, TaskテーブルをDrop & Create
  await models.Project.sync({ force: true });
  await models.Task.sync({ force: true });

  // projectインスタンス作成
  const project = models.Project.build({
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man'
  });

  // projectのinsert
  const createdProject = await project.save();

  // insertされたprojectにひもづくTaskを作成
  await createdProject.createTask({
    title: 'title',
    description: 'description'
  });

  // projectのselect
  const projects = await models.Project.findAll({
    include: [models.Task]
  });
  console.log(projects.map(d => d.toJSON()));

  // taskのselect
  const tasks = await models.Task.findAll({
    include: [models.Project]
  });
  console.log(tasks.map(d => d.toJSON()));
})();

npx ts-node src/main.tsを実行すれば下記のように出力されるであろう。

Executing (default): SELECT `Project`.`id`, `Project`.`title`, `Project`.`description`, `Project`.`createdAt`, `Project`.`updatedAt`, `Tasks`.`id` AS `Tasks.id`, `Tasks`.`title` AS `Tasks.title`, `Tasks`.`description` AS `Tasks.description`, `Tasks`.`deadline` AS `Tasks.deadline`, `Tasks`.`projectId` AS `Tasks.projectId`, `Tasks`.`createdAt` AS `Tasks.createdAt`, `Tasks`.`updatedAt` AS `Tasks.updatedAt` FROM `project` AS `Project` LEFT OUTER JOIN `task` AS `Tasks` ON `Project`.`id` = `Tasks`.`projectId`;

[ { id: 1,
    title: 'my awesome project',
    description: 'woot woot. this will make me a rich man',
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Tasks: [ [Object] ] } ]

Executing (default): SELECT `Task`.`id`, `Task`.`title`, `Task`.`description`, `Task`.`deadline`, `Task`.`projectId`, `Task`.`createdAt`, `Task`.`updatedAt`, `Project`.`id` AS `Project.id`, `Project`.`title` AS `Project.title`, `Project`.`description` AS `Project.description`, `Project`.`createdAt` AS `Project.createdAt`, `Project`.`updatedAt` AS `Project.updatedAt` FROM `task` AS `Task` LEFT OUTER JOIN `project` AS `Project` ON `Task`.`projectId` = `Project`.`id`;

[ { id: 1,
    title: 'title',
    description: 'description',
    deadline: null,
    projectId: 1,
    createdAt: 2019-07-21T04:23:17.000Z,
    updatedAt: 2019-07-21T04:23:17.000Z,
    Project: 
     { id: 1,
       title: 'my awesome project',
       description: 'woot woot. this will make me a rich man',
       createdAt: 2019-07-21T04:23:17.000Z,
       updatedAt: 2019-07-21T04:23:17.000Z } } ]

さいごに

ts 環境の構築と sequelize の説明両方が入ってしまったので、ややこしくなってしまった感いなめないが、何かの参考になればと思う。

associateなど sequelize の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。

Sequelize入門

SequelizeというORMについて知識の整理のため、記録を残そうと思う。Databaseはmysqlを使用する。

今回は、
1. database作成
2. table作成
3. insert, update, delete
という基本的なことの整理をしたい

1. databaseの作り方

databaseを作成するのは、sequelizeではなく、sequelize-cliを利用するので、両方インストールする。

npm i -D sequelize-cli sequelize mysql2

以下のファイルを作成する。

config.json

{
  "username": "root",
  "password": "****",
  "database": "sequelize",
  "host": "127.0.0.1",
  "dialect": "mysql"
}

次のコマンドを叩く
npx sequelize db:create --config config.json

$ npx sequelize db:create --config config.json
Sequelize CLI [Node: 8.12.0, CLI: 5.5.0, ORM: 5.10.0]
Loaded configuration file "config.json".
Database sequelize created.

注意として、npxに続くのがsequelize-cliではなく、sequelizeであること。

これでsequelizeというdatabaseが作成される。

2. tableの作成

main.jsを作成する。

main.js

const { database, username, password, host, dialect } = require("./config");
const Sequelize = require("sequelize");
const { Model } = Sequelize;

// database接続
const sequelize = new Sequelize(database, username, password, {
  host,
  dialect
});

// Model定義
class Project extends Model {}
Project.init(
  {
  title: Sequelize.STRING,
  description: Sequelize.TEXT
  },
  { sequelize, modelName: "project" }
);

(async () => {
  // Projectテーブル作成
  await Project.sync({ force: true })
})()

node main.jsと実行すると 大文字、小文字のsequelizeが出てくるので混乱するかもしれないが、 大文字がSequlizeライブラリとしての情報を、小文字がdatabase接続した情報をもっていると理解すればよいのであろう。

Project.initでORMのMを定義している。

この場合、
- String型のtitle
- Text型のdescription
をもつテーブルを作成しようとしてる。

さらにORMによって自動的に、id(PK)やcreateAt, updateAtも作成される。 createAd, updateAtをつけたくない場合は、timestampsプロパティを利用して、 {sequelize, modelName: 'project', timestamps: false}と記述すればよい。

Project.sync()でtable作成しており、{force: true}を与えることでdrop & createとなる。

insert, update, deleteの実行

以下main.jsの続きとして書いていきます。

1レコードの場合

(async () => {
  
  // instanceを作成する。ここではまだinsertされていない。
  const project = Project.build({
    title: "my awesome project",
    description: "woot woot. this will make me a rich man"
  });
  
  // insert
  const created = await project.save(); // (1)
  // update
  const updated = await project.update({  // (2)
    title: 'update title'
  });
    
  // delete
  const deleted = await project.destroy(); // (3)
})()

帰ってきた値のtoJSON()メソッドを適用すると以下の結果を受け取れる。

(1) insertされたレコードの情報を取得できる(created.toJSON())

{ 
  id: 1,
  title: 'my awesome project',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.720Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(2) updateされたレコードの情報を取得できる(updated.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

(3) deleteされたレコードの情報を取得できる(deleted.toJSON())

{ 
  id: 1,
  title: 'update title',
  description: 'woot woot. this will make me a rich man',
  updatedAt: 2019-07-14T13:44:17.751Z,
  createdAt: 2019-07-14T13:44:17.720Z 
}

複数レコードの場合

update, destoryは複数レコードに対応できるが、insertの場合、 bulkCreateというメソッドを利用する。

main.js

(async () => {
  const results = await Project.bulkCreate([
    { title: "programming", description: "executing" },
    { title: "reading", description: "executing" },
    { title: "programming", description: "finished" }
  ]);
  console.log(results.map(d => d.toJSON()))
})()
[ 
  { id: 1,
    title: 'programming',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 2,
    title: 'reading',
    description: 'executing',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z },
  { id: 3,
    title: 'programming',
    description: 'finished',
    createdAt: 2019-07-14T13:53:49.986Z,
    updatedAt: 2019-07-14T13:53:49.986Z } 
]

さいごに

次は、whereやgroup by句やテーブルのjoinなどのselectに関するものをまとめようと思う。 joinこそがORMで一番理解が難しいところだと思ってる。

dockerでnginx を使ってリバースプロキシをたてる

概要

前回node プロジェクトの dokcer image を作成した。
express などで作成した node でサーバは well known port を使用できないので、80 ポートで HTTP 通信するには、nginx などの web サーバが必要になる。

今回はnginx でリバースプロキシのコンテナを作成しnode のサーバと通信できるようにする。

nginx設定

nginx に行ってもらいたいことは、

  1. 80 ポートで HTTP リクエストをうけいれる
  2. 受けたリクエストを Backend の特定ポートでまつサーバになげる
    である。

Backend のサーバには、3000 ポートで待機する node サーバを利用する。/api で{hello: "world"}を返すだけの単純なサーバである。

最小限の修正で確認するために次のファイルを作成する。

default.conf

server {
    listen 80;
    server_name localhost;
    location / {
        proxy_pass http://app:3000; # (1) appはコンテナ名にする
        proxy_pass_request_headers on;
    }
}

proxy_passに Backend で待機するサーバのホスト名とポート名を記述する。
同一サーバ内であれば、localhost:3000 と記述すれば Backend に流してくれるが、コンテナを使う場合 localhost ではアクセスできない。
代わりコンテナ名が必要となり、ここではappと決め撃ちする。
これで設定ファイルは作成できたので次にコンテナを起動していく。

node コンテナの起動

先に backend のサーバを起動するため、以下を実行する

docker run -d --name node -p 3000:3000 tsweb tsweb は、express x typescript で作成した web サーバイメージである。

nginx コンテナ起動

backend サーバと接続できるように以下を実行する

docker run -d --name web -v ~/conf.d:/etc/nginx/conf.d -p 80:80 --link node:app nginx

先程作成したdefault.confを格納したディレクトリを-vオプションで指定し、コンテナ内の/etc/nginx/conf.d/default.confを上書きして起動する。

--linkオプションを利用して、app という名前で node コンテナにアクセスできるようにしている。default.confで記述したhttp://app:3000も名前解決できるわけである。

docker psの出力結果

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
d36e0f3a8885        nginx               "nginx -g 'daemon of…"   3 minutes ago       Up 3 minutes        0.0.0.0:80->80/tcp       web
fe6e3c3beae5        tsweb               "/bin/sh -c 'npm sta…"   6 minutes ago       Up 6 minutes        0.0.0.0:3000->3000/tcp   node

curl localhost/apiと入力して期待した json が帰ってくることを確認できる。

docker-composeなら

docker-compose で記述するなら以下の通りとなる。

docker-compose.yml

version: "3"
services:
  web:
    image: nginx
    depends_on:
      - api
    volumes:
      - ~/conf.d:/etc/nginx/conf.d
    ports:
      - "80:80"
  api:
    image: tsweb
    expose:
      - "3000"

depends_onを利用して、nodeコンテナ起動後に、nginxコンテナを起動させている。

以上で nginx を使ってリバースプロキシコンテナを起動できることを確認できたので、次は conf を整理した image を作成して利用すればよい。

node プロジェクトの docker image をつくってみる。

社内でAWS ECS を使うことになったが、そもそも dokcer image を作ったことがなかったので
調べてみた。

目次

  1. node プロジェクトの用意
  2. Dockerfile の作成と build
  3. image からコンテナ起動
  4. docker-compose 起動

1. node プロジェクトの用意

typescript & Express を利用した簡単な web サーバを用意する。 port 番号で 3000 で起動して、localhost:3000/api に HTTP GET リクエストするとslack に通知がくるものだ。

フォルダ構成は以下の通り

.
├── __test__
├── jest.config.js
├── node_modules
├── nodemon.json
├── package-lock.json
├── package.json
├── src
└── tsconfig.json

2. Dockerfile とコンテナイメージの作成

alpine という軽量 Linux ディストリビューションを利用する。 この上に nodejs と npm をインストールし、ローカルで開発した node プロジェクトを含めるようにする。そして、npm installnpm startを起動させている。

touch Dockerfileでカレントディレクトリにファイルを作成して次の通り記述する。

Dockerfile

FROM alpine:latest

# instal llatest nodejs and npm
RUN apk add --update nodejs nodejs-npm

# make entry directory
RUN mkdir myapp
WORKDIR /myapp

# copy localfile to images
ADD src/ src/
ADD nodemon.json .
ADD package.json .
ADD tsconfig.json .
RUN npm set progress=false && \
  npm install
CMD npm start

npm set progress=falseは本質的ではないので、なくてもよい。
これはnpm installの進捗報告を出力しない設定で起動を速くしているために記述している。

Dockerfile があるディレクトリから以下を実行する
docker build -t tsweb .

-t オプションはイメージに名前をつける。tag の略称だ。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tsweb               latest              1a656139af79        6 seconds ago       32.9MB
alpine              latest              4d90542f0623        3 days ago          5.58MB

3. コンテナ起動

以下のコマンドを叩く。 docker run -d --name web -p 8000:3000 tsweb

curl localhost:8000/apiと叩けば、次の通りslack通知がくる。 f:id:poppon555:20190623220312p:plain

4. docker-compose 起動

蛇足になるが、docker-compose を利用して同じようにコンテナを起動してみる。

docker-compse.yml

web:
  image: tsweb
  ports:
    - '8000:3000'

docker-compose up -dでバックグラウンドでコンテナ起動。

最後に

ローカル環境で docker image を作成してコンテナ起動までの手順を紹介した。
今度は ECR に image を登録して、ECS で利用してみるところまで紹介したい。

参考にしたサイト

docker run -vとDockerfileのVOLUMEは違うのか?

はじめに

最近dockerについてプログラマのためのDocker教科書(以下教科書と呼ぶ)で勉強し始めた。

今回は、dockerを使ってデータの永続化という話でつまづいたときの整理をしたいと思う。

データの永続化とは、たとえばmysqlコンテナとデータコンテナを分けるということである。
たとえ前者を削除しても、データコンテナは別なのでデータを失うことはない。 別のバージョンのmysqlをデータコンテナにリンクさせることでポータビリティを保てるというメリットがあるわけだ。

教科書でデータ用のコンテナを作成するときに、次のDockerfileからイメージをビルドして作成している。

Dockerfile

# Dockerイメージの取得
FROM centos:latest

...(中略)...

# ボリュームの作成
VOLUME /var/log/httpd

しかしここでいくつか気になる疑問がでてきたのだ。
- ファイル内のVOLUMEが指すものは、どこに存在するものなのか?(ホストのどこか?)
- Dokcerfileでイメージをビルドしないとデータ永続用コンテナは作成できないのか?
- docker runのvオプションとはVOLUMEは別物なのか?

この疑問に関して調べた結果を残そうと思う。 先に結論を述べると、vオプションでもVOLUMEも同じである。

docker run vオプションについて

教科書には、vオプションについて次のように書かれてある。

docker run -v [ホストのディレクトリ]:[コンテナのディレクトリ]
ホストとコンテナでディレクトリを共有化する方法。

なるほど、この場合データの実態はホストに存在している。

では、次のように書くと何を意味するのか?
docker run -v /myvol -e MYSQL_ROOT_PASSWORD=mysql --name dbserver mysql

下図の通り確かにmyvolが作成されたコンテナに存在している。
が、それはどこに繋がれてる? ホストなのか?

f:id:poppon555:20190504112607p:plain

実は、/myvolは、docker volumeに接続されている。 docker volume lsと入力すると、dockerで作成されたボリュームができる。

f:id:poppon555:20190504112714p:plain

ただし-v /myvolとした場合、volume名がハッシュ化されてどの用途のボリュームなのかがわからなくなる。上図でも一つはmysqlコンテナ自体が使用しているボリュームで、もう一つは/myvolにマウントしているボリュームになるが、ハッシュ化されたVOLUME NAMEではわからない。

名前解決できない状況を回避するためには、ボリューム名を明示的に指定して作成し、それをマウントさせる。
docker volume create v1
docker run -v v1:/myvol -name dbserver mysql

v1という名前のボリュームの作成

f:id:poppon555:20190504113741p:plain

作成したボリュームをマウントする docker run -d --name dbserver -v v1:/myvol -e MYSQL_ROOT_PASSWORD=root mysql

f:id:poppon555:20190504112737p:plain

docker volume lsで確認

f:id:poppon555:20190504112752p:plain

以上よりdocker runのvオプションについてまとめると、
docker run -v [ホストのディレクトリ]:[コンテナのディレクトリ] mysql ホストと共有化
docker run -v [dokcer volume]:[コンテナのディレクトリ] mysql 名前付きボリュームを指定ディレクトリにマウント
docker run -v [コンテナのディレクトリ] mysql ハッシュ値のボリュームを指定したディレクトリにマウント
となり、最後の2つについては、起動コンテナと別のボリュームがマウントされ、名前付きかどうかが異なる

補足: rmオプションについて
docker rmでコンテナを削除しても、vオプションで作られたボリュームは同時に削除されない。

f:id:poppon555:20190504112808p:plain

コンテナが不要になったと同時に削除するには、rmオプションをつけて、docker run --rm -v v1:/myvol mysql と入力する。

f:id:poppon555:20190504112821p:plain

※それでもマウントしたv1ボリュームは削除されないことは注意

.DockerFileのVOLUMEについて

教科書には、VOLUMEについてこう書かれている。

VOLUME ["/マウントポイント"]
指定したマウントポイントをコンテナ内に作成し、ホストやその他コンテナからボリュームの外部マウントを行います

よくわかりませんね。。。
なので、実際にDockerfileをつくってやってみましょう。

.DockerFile

FROM mysql
VOLUME /myvol
ENV MYSQL_ROOT_PASSWORD mysql

docker build -t dbesrver .でイメージ作成して、
docker run -d -name db dbserverでコンテナ起動
マウント状況を確認すると、

f:id:poppon555:20190504112837p:plain

上図の通り、Dockerfileを利用した場合も新たにボリュームを作成して、VOLUMEで指定したディレクトリにマウントするという意味である。ボリュームに名前をつけていないので、起動したコンテナのボリュームの名前をハッシュ値となる。 (したがって、ボリュームが不要ならば--rmオプションを付けた起動したほうがよい。)

結果

以上まとめて、はじめの疑問に回答すると

  1. ファイル内のVOLUMEが指すものは、どこに存在するものなのか?(ホストのどこか?)

    ホストに存在するが特定のディレクトリと共有してるわけではなく、
    docker volumeとして作成された新たにボリュームを指している

  2. Dokcerfileでイメージをビルドしないとデータ永続用コンテナは作成できないのか?

    docker run -vからも作成可能

  3. docker runのvオプションとはVOLUMEは別物なのか?

    同じである。 名前付きかどうかという違いがあるぐらいだ。

というわけでした。

参考サイト

詳しく書かれてあったのでわかりやすかったです。

https://qiita.com/namutaka/items/f6a574f75f0997a1bb1d https://qiita.com/mtgto/items/aabc212a1d3f99878828 https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32

jsでオブジェクト配列をつかった集合演算

はじめに

jsで集合演算についてしらべてみた。
調べた限り集合演算の例は[1,2,3]と[2,3,4]の積集合をもとめるといったプリミティブ値の配列が多くて、オブジェクト配列をつかった 集合演算が見つからなかったので、残して見ようと思う。

問題(特定の本の返却期日をしらべる)

たとえば、ある人が本屋から複数の本をレンタルしてたときを考えてみる。
レンタルしている本は、id、名前とサイズともつ(A)
本屋はレンタルした本のidと返却期日を保管している(B)

レンタルしてる本からサイズが小さいものの返却期日を調べるにはどうすればよいか?
(A)の部分集合の返却期日を(B)から得たいというわけです。

これらをコードで記述すれば、

// (A) レンタルしてる本一覧
const books = [
  { id: 1, name: 'book1', size: 'large' },
  { id: 2, name: 'book2', size: 'small' },
  { id: 3, name: 'book3', size: 'medium' },
  { id: 4, name: 'book4', size: 'medium' },
  { id: 5, name: 'book5', size: 'small' },
];

// (B) レンタルしている本の返却期日一覧
const lends = [
  { id: 1, end_on: '2019-01-01' },
  { id: 2, end_on: '2019-02-01' },
  { id: 3, end_on: '2019-01-11' },
  { id: 4, end_on: '2019-01-22' },
  { id: 5, end_on: '2019-01-11' },
];

サイズがsmallのものは、idが2,5である。
これの返却期日をlendsから得ると次のとおりになる。

// もとめたいもの(サイズがsmall)
[
  { id: 2, end_on: '2019-02-01' },
  { id: 5, end_on: '2019-01-11' },
]

解決方法

books
  .filter(book => book.size === 'small')  // (1)
  .map(target => lends.find(lend => lend.id === target.id)) // (2)

(1): booksから特定サイズの部分集合をえる

(2): (1)で得た部分集合のidと同じidをもつものをfindで取得してmapで配列を返す

さいごに

jsでオブジェクト配列から積集合?をもとめる書き方を紹介してみました。

使えるときがあるのかな笑
SQLで書くと、

select L.id, L.end_on from books B where size = 'small' left join lends L on B.id = L.id

を求めたいわけでした。

jsで集合演算というとSetという演算子がつかえるみたいですね。
Setをつかって今回の問題はとけるのかな...

flow-typedで型情報を管理しやすくしてみた

flowで型を導入後、毎回importするのが面倒だなと感じるようになってきた。
flow-typedをつかえば型情報をグローバルに定義できるようなので、これでimport文書く手間をカットしたり型情報をうまく管理できるのでは...

ということで今回は、flow-typedをつかって型情報を楽に管理できる方法について書き残そうと思う。

目次

  1. flow, eslintを使えるように準備
  2. flow-typedのインストール
  3. flow-typedディレクトリ作成と.flowconfigの記述
  4. 定義ファイルをつくって動作確認
  5. サードパーティライブラリの型定義を利用してみる

1. flow, eslintの準備

以前作成した記事通りに準備します。 本記事では、割愛させてもらいます。

2. flow-typedのインストール

npm i -D flow-typedを実行する

3. flow-typedディレクトリと設定ファイル

下図のようにrootディレクトリにflow-typedというディレクトリを作成する。
このディレクトリに型定義ファイルを保存していく。
自分で作成したものやreduxなどのサードパーティのライブラリものが保存される。

.
├── .eslintrc.json
├── .flowconfig
├── flow-typed <- つくりました
├── node_modules
├── package-lock.json
├── package.json
└── src

.flowconfig

[ignore]

[include]

[libs]
flow-typed
[lints]

[options]

[strict]

型定義ファイルを保存したディレクトリがどこか指定する。
上記のとおり[libs]に対して、先程作成したflow-typedディレクトリを記述する

4. 型定義ファイルをつくって動作確認

1. 直下にファイルを作成する

flow-typed/sampleType.js

type SampleType = {
  id: number,
  name: string
};

src/sample.js

const obj: SampleType = {
  id: '21',  <- (1)
  name: "SampleName"
};

sample.jsファイル内にimport type {SampleType} from './path'の記述はない
そして、(1) は数値ではないので、エラーとなる。

2. ディレクトリをきってもglobalにロードしてくれる

flow-typed/myModule/myType.js

declare type MyType = {
  uid: number,
  address: string,
  tel: number,
};

これでimport文なしに型チェックしてくれる。

4. サードパーティライブラリの型定義を利用してみる

./node_module/.bin/flow-typed install redux@4
で公開されたreduxの型情報をダウンロードできる。
ファイルは、flow-typedディレクトリに作成される。 あとは、3.の同様に使用できる。

しかし、この場合global空間を汚染させないように作られるため、import文は必要となる。

さいごに

でもflowよりもtypescriptのほうが、人気なんだよね...

参考サイト

flow公式
flow-typed