awkでvlookupみたいな表結合させる

はじめに

最近、bash, awk, jqにお世話になっております。

特にawkは使い始めたばかりなので、awkを使ってワンラインでexcelのvlookup的なことをやりたいときにどうすればよいのかわからなくて、excelで頑張ってたりしてました。

でもエンジニアならば、excelなんて使わなくてもコマンドラインでぱぱっと操作できたほうが格好いいじゃない?と思ったので挑戦してみます。

目標

以下の2つの表を想定する。

ユーザ一覧を格納した表(users.csv)

id name
-- ----
1  一郎
2  二郎
3  三郎
4  四郎

何かしらの条件をみたしたIDの表(condition.csv)

id
--
2
4

この2つの表から以下を得たい。

ゴール

id name condition
-- ---- ----
1  一郎 F
2  二郎 T
3  三郎 F
4  四郎 T

conditionにデータがあれば、usersの3列名にTを立て、なければFを立てたいわけです。

sqlでいえば、

SELECT
  id
  , name
  , CASE WHEN condition.id IS NULL THEN 'F' ELSE 'T' END
FROM users
LEFT JOIN CONDITION
ON users.id = CONDITION.id

みたいな操作を実行したいのだ。

回答

awk 'FNR==NR{a[$1]++; next} {print $0, (a[$1]) ? "T" : "F"}' condition.csv users.csv

ね、簡単だね!

説明

なぜ上記コマンドになるのかを説明しようと思う。

FNR==NR{a[$1]; next}

FNR==NRは条件を表現しており、{}内はその条件がマッチしたときの処理を意味する。

よって、FNR==NR, {}内に分けて説明する。

1. FNR==NRとについて

2つのファイルを読み込んで、それぞれNR,FNRと両者の行を出力すると、以下となる。

awk '{print NR, FNR, $0}' condition.csv users.csv
NR FNR $0
-- --- --
1   1   2
2   2   4
3   1   1 一郎
4   2   2 二郎
5   3   3 三郎
6   4   4 四郎

NRは2つのファイルをまとめて何行目を処理しているかを FNRは各ファイルの何行目を処理しているかを表している。

よって、はじめのファイル(condition.csv)が処理中のときは、FNR==NRが真となる。

2. {a[$1]++; next;}

突然登場した a は変数である。var, constなど使わずともいきなり変数を定義できるのである。さらに[$1]とつづくので、この変数は$1をkey名にもつ連想配列になる。
++は、1値を追加せよ(初期値0)という意味になる。

したがって、aという連想配列を宣言して、$1をkeyとして1増加せよという意味である。

next は、以降つづく処理をストップして次の行に進みなさいという意味である。{a[$1]++; next;} {…1} {…2} {…n}と続いたときに{…1}から{…n}の操作は実行されない。

3. {print $0, (a[$1]) ? "T" : "F"}' condition.csv users.csv

今までのを整理すると、 FNR==NR{a[$1]++; next} {...} は、

  1. はじめに読み込んだファイルに対してのみ真となり、
  2. 読み込んだ1列目をkeyとして1追加した連想配列を生成し、
  3. 次に続く処理{…}を実行しない

という意味になる。

残った {print $0, ....} は、FN==FNRが未達成ときに実行される。つまり、NR >= 3のとき、常に処理される。

$0で、users.csvの処理中の行を表示し、 a[$1]は、users.csvの1行目の値を、連想配列aのkeyに指定したときに真となるかどうかを確認して、TまたはFを出力している。

注意

users.csvとcondition.csvの順番を間違えると意味がなくなるので、注意が必要である。

awk 'FNR==NR{a[$1]++; next} {print $0, (a[$1]) ? "T" : "F"}' users.csv condition.csv

結果は、こちらの通り。理由はもうわかりますよね。

id Flag
-- --
2  T
4  T

このやり方を使うときは、必ずSQLでいうRihgt joinになってしまうわけである。users.csvが4行あるのに対して、conditionは2行しかないのでおかしな結果になってしまう。

もっとつぎへ

先程の使用したconditionで該当するユーザが、2,4だけであることが記載されていた。 しかし、そうでないときもあると思います。

たとえば、bmi.csvが、全ユーザのBMI情報を保存しており、25以上ユーザの名前とBMI情報が知りたいということもあります。

全ユーザとBMI情報が記載された表(bmi.csv)id = 2, 4は

id BMI
-- --
1  22
2  40
3  21
4  30
awk 'FNR==NR{a[$1]=$2; next} {if (a[$1] > 25) print $0, a[$1]}' bmi.csv users.csv

今回のように、bmi.csv, users.csvの行数が同じならば、LEFT JOIN的に記述することも可能となります。ちょっと冗長かな?

awk 'FNR==NR{a[$1]=$2; next} {if ($2 > 25) print $1, a[$1],  $2}' users.csv bmi.csv

まとめ

awkをつかって、特定の列をキーに2つの表を結合する方法を整理してみました。

ファイル内容によっては、ファイルの指定順序に気をつけて使ってみてください。

とはいうものの、csvが用意されていたらつかえるわけで、 結局excelスプレッドシートで渡されるようであれば使えないわけですね。

そのあたりはなんとかできないんかな汗

typescript導入したprivateなnpmパッケージの作り方

はじめに

開発の規模を大きくなってくると、共通化したコンポーネントを利用したいこともあると思います。
git submoduleをつかって共通部分を切り出すことも可能ですが、branchの変更忘れてしまうと反映されないので、個人的には好みではないです。 一方privateなnpmパッケージで実現することも可能だと思います。

npm private registoryを利用することも可能ですが、こちらの場合 US $7/月という月額料金がかかってしまいます。 一方、github privateは無料になったので、githubをつかってprivate npmパッケージを作ってみたいと思います。

また共通利用するなら型情報があったほうがありがたいので、typescriptを導入してみたいと思います。

目標

  1. github privateリポジトリの作成
  2. 他のリポジトリからimport確認
  3. npm version を利用してバージョン管理する

1. my private npm moduleの作成

npm moduleの作成

共通ライブラリのnpmパッケージを作っていきます。

  1. npm moduleの作成
mkdir myNpmModule
cd myNpmModule
git init
npm init -y

.gitignoreの設定は割愛します。

  1. typescript設定
npm i -D typescript
npx tsc --init

npx tsc --init で自動生成された.tsconfig.jsonに以下追加してコンパイル設定を修正する。

.tsconfig.json

 {
 ...
 "declaration": true,
 "sourceMap": true,
 "outDir": "./build",
 "rootDir": "./src",
 ...
}

rootDir でbuild対象のファイルを指定し、outDir でbuild後のファイル保存先を指定する。

declaration でビルド時にxxx.d.tsとして型定義ファイルを出力させる。
sourceMap でtsとjsの対応関係をつくっておく。

  1. package.json修正

共通ライブラリの設定を追加します。

package.json

{
  ...
  "main": "build/index.js",
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build"
  },
  ...
}

files ではパッケージ利用側にどのディレクトリ・ファイルをダウンロードさせるかを指定できる。
ただし、package.jsonとREADME.mdは指定しなくてもダウンロードされる。

main でパッケージ利用側がimportコマンドを使用したときにどのファイルをロードするのかを指定する。

scripts では、typescriptのbuildコマンドに加えて、 prepare を設定する。

prepare はパッケージ利用側が、パッケージをインストールする直前に実行させる処理を定義できる。
srcからビルドされたファイルを作成してくれるため、パッケージ管理側はbuildファイルをgit管理しなくてよい。

つまり、利用側で勝手にsrcからbuildディレクトリを生成してくれるため、.gitignoreにbuildディレクトリを指定しなくてよいわけとなります。

  1. 公開モジュール作成
mkdir src
touch src/Person.ts

src/index.ts

class Person {
  private name: string;
  private age: number;

  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
      return this.name;
  }
}

export { Person };
git add .
git commit -m 'my first commit';

github private repoとして登録

githubページより、新しいリポジトリ作成から、privateにチェックをつけて作成する.

f:id:poppon555:20200405122203p:plain

git remote add origin git@github.com:karuta0825/myNpmModule.git
git push -u origin master

f:id:poppon555:20200405122217p:plain

buildディレクトリ管理しなくてよしです。

2. 他のリポジトリからimport確認

package.json

{
  "dependencies": {
   ...
   "myNpmModule": "git+ssh://git@github.com:karuta0825/myNpmModule.git",
   ...
  },
}

利用側のpackage.jsonのdepenenciesで、使用したいprivate moduleを指定する。git+ssh//に続けて、githubのclone or dowloaddに表示されているパスをコピーすればよき。

f:id:poppon555:20200405122242p:plain

npm i を実行すると、node_modulesに作成したprivate packageが作成されてます。

.
├── node_modules
│   ├── @types
│   ├── myNpmModule <-- 追加されてる
│   └── typescript
├── package-lock.json
├── package.json
├── sample.ts
└── tsconfig.json

node_modulesのmyNpmModuleのディレクトリ構成も設定したとおり。

myNpmModule
├── build
│   ├── index.d.ts
│   ├── index.js
│   └── index.js.map
└── package.json

package.jsonのfilesに指定したbuildディレクトリのみがあり、そこには、型定義ファイルのindex.d.tsとコンパイルされたjsとsourceMapの.mapファイルがある。

では、実際にインストールした共通パッケージを利用できるか確認してみます。

f:id:poppon555:20200405123146p:plain importでき、候補も表示されています。

3. my npm moduleをバージョン管理する

共通パッケージを使用していくならば、バージョン管理をしておくも考えられると思います。
ということで、npmのバージョン管理設定も説明しておきます。
とっても簡単。commit後に npm version を使用するだけです。

import dayjs from "dayjs";

class Person {
  private name: string;
  private age: number;
  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
    return this.name;
  }

  // 追加
  public tellBirthYear(): string {
    return dayjs().subtract(this.age, "year").format("YYYY");
  }
}

git commitする

git add .
git commit -m 'add a method to Person class.'

npm versionのあとに、major, minor, patchのいずれかを指定できます。
majorだと、1.0.0 => 2.0.0
minorだと、1.0.0 => 1.1.0
patchだと、1.0.0 => 1.0.1
にバージョンが変わる。

package.jsonのversionや、git logが変更されていること確認できます。

npm version minor

git log --oneline
// 6a0bb79 1.1.0
// 3d505ff add a method to Person class.
// 351675f my first commit

package.json

{
  ...
  "version": "1.1.0",
  ...
}

git pushして、利用側でnpm updateをして確認すると、

npm update

+ myNpmModule@1.1.0
added 1 package from 1 contributor, updated 3 packages and audited 5 packages in 19.281s
found 0 vulnerabilities

1.1.0のmyNpmModuleが利用できるようになりました。

実際下図の通り、追加したメソッドが候補に上がるようになります。

f:id:poppon555:20200405122235p:plain

まとめ

github privateを利用して、typescript導入npmパッケージを作成しました。
また npm version を利用してバージョン管理する方法をかんたんに説明しました。

ライブラリとして使用されるならば、以下のことも今後対応方法見つけられたらと思ってます。

  • パッケージのmin化 現状だとsrcのディレクトリ構成のままbuildディレクトができるので、一本にまとめたほうがよい。
  • material-uiやlodashのようにmyNpmModue/Personみたいな感じでロードファイルを指定できる

参考にしたサイト

https://dev.classmethod.jp/articles/private-npm-modules-to-package-json/https://medium.com/cameron-nokes/the-30-second-guide-to-publishing-a-typescript-package-to-npm-89d93ff7bccd

goで継承をつかう

はじめに

goを勉強始めるとjavaなどで学習してきたオブジェクト指向の考え方を違う場面に出くわす。
たとえば、goでは継承できないことである。
しかし委譲を使えば、同じことを実現できるのでその整理をしようと思う。

仕様整理

下図のような関係を考える。 Personクラスはスーパークラスで、Manクラスをそのサブクラスという関係である。
Personクラスは、名前と年齢をメンバ変数にもち、getName()で名前を取得でき、oldAge()でひとつ年を増やせるメソッドをもつ。
一方Manクラスは、性別("M", "S")をもち、男ならばcall()メソッドで"Mr. 名前"と返す。

f:id:poppon555:20191222141722p:plain

goで実装

goではクラス/メソッドではなく、struct/receiverと呼ぶが、クラスを使う。

package main

// スーパークラス
type Person struct {
  name string
  age  int
}

// コンストラクタ
func newPerson(name string, age int) *Person {
  return &Person{
    name,
    age,
  }
}

func (p *Person) getName() string {
  return p.name
}

func (p *Person) oldAge() {
  p.age = p.age + 1
}

// サブクラス
type Man struct {
  // (1) Personポインタ 
  *Person
  sex string
}

// コンストラクタ
func NewMan(name string, age int) *Man {
  return &Man{
    // (2)  ポインタ変数にインスタンスを格納
    Person: newPerson(name, age),
    sex:    "M",
  }
}

func (m *Man) call() string {
  return "Mr." + m.name
}

(1) Personポインタを渡すことでManインスタンスから直にPersonインスタンスにアクセスできる 。
man.Person.getName() ではなく、man.getName() が可能となる。
Personの部分をショートカットしてgetName()が呼べるので、あたかも継承してるかのように使える。
もし、person Person とメンバ変数名を明記すると、man.Person.getName()でアクセスしないといけないので継承ぽさは消える。

(2) サブクラスでスーパクラスのインスタンスを作成するとき、*を外したPersonにインスタンスを格納する。

testコードでうまくいくことを確認できる

package main

import "testing"

func TestNewMan(t *testing.T) {

  t.Run("スーパクラスのメソッドgetName()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.getName() != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("スーパクラスのメソッドoldAge()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    man.oldAge()
    if man.age != 11 {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスからスーパークラスのメンバ変数nameにアクセスできる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.name != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスの定義メソッドcall()が呼べてMr.adamと返す", func(t *testign.T) {
    man := NewMan("adam", 10)
    if man.call() != "Mr.adam" {
      t.Fatal("err")
    }
  })
}
PASS
ok      github.com/karuta0825/study 0.262s

最後に

オブジェクト指向脳の人が、goで継承ぽいものを使う方法を説明しました。
goはコンパイル言語なのに、スクリプト言語のように開発すすめるのはいい言語だなと思います。
javaのライブラリ管理とか面倒だもの。
もっともgenericsが使えないのは、悔しいのですが。。。
そんな感じでgoらしさというのがまだまだわからずなのですが、来年はgoへの理解を深めていきたい!

述語論理をつかったSQL

はじめに

僕は、SQLが苦手である。 普段使い慣れている言語とは別の考えがSQLには必要なのであろうと痛感している。

ということで、達人に学ぶ SQL徹底指南書を使って最近SQLの考え方を学習中。
今回は、SQLで数列を扱うの備忘録を残す。

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

問題

以下の座席表から空席が 3 つ連続している部分を求めろ たとえば、(3,4,5), (7,8,9)となる。

id status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

データの準備

CREATE TABLE seats
(
    seat   INT NOT NULL,
    status VARCHAR(10),
    PRIMARY KEY (seat)
);

INSERT INTO seats
VALUES (1, ""),
       (2, ""),
       (3, ""),
       (4, ""),
       (5, ""),
       (6, ""),
       (7, ""),
       (8, ""),
       (9, ""),
       (10, ""),
       (11, ""),
       (12, ""),
       (13, ""),
       (14, ""),
       (15, "");

回答

SELECT
  s1.seat as '始点',
  s2.seat as '終点'
FROM seats s1,
     seats s2
WHERE s1.seat + 2 = s2.seat
  AND NOT exists(
        SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '')
    );
始点 終点
3 5
7 9
8 10
9 11

考え方

始点、終点を s1, s2 の自己結合によって得、その間に s3 のすべての status が空を満たすというクエリをつくる方針をとる。

すべての〇〇が XX という条件を満たすというクエリを記述するのにAll演算子を使おうと考えるが、MySQLにはAll演算子は用意されていない。

なので、EXSITS演算子を利用して All と同じ意味をもつ式を記述することになる。
いきなりだが、以下は述語論理から成立する式である。

1. ∀xF(x) => ¬∃x¬F(x)
2. ∃xF(x) => ¬∀x¬F(x)

∀ は All を意味するので、今回は 1 のパターンである。
この考え方を日本語で整理すると、
始点・終点間のすべての座席が空である

始点・終点間に空ではない(a)座席は存在しない(b)
と表現可能となる。

(a), (b)をクエリで記述すると

  1. SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '空')
  2. NOT exists(1.)

となる。

よって、上記回答の通りになる。

おわりに

にしてもSQLはクエリに悩むよりも、データの準備が大変だ。。。 もっと楽にデータ準備できないものかと感じる

DataGripでクエリの大文字化設定

SQLのエディタ?は、Datagripを使うようになった(勉強中)

その理由は、 1. キーバインドがカスタマイズできる 2. 補完が最強 3. フォーマット機能が優秀 にある。

さらに、クエリを自動で大文字化してくれることがわかったので 載せておこう。

Datagrip > Preferences > Editor > Code Style > SQL > データベース にあるcaseタブから設定できる

f:id:poppon555:20191014181802p:plain

これが、自動フォーマット(Cmd + option + L)させると f:id:poppon555:20191014182332p:plain

こうなる f:id:poppon555:20191014182427p:plain

ありがたや。 DataGripもっと使いこなせるようになりたい。。。

SQLの集合演算整理

はじめに

SQL は集合的思考が必要ということだが、和集合や差集合など求める集合演算の仕方は複数存在するようなので、一度整理しようと思う。

  1. 集合演算子形式
  2. exist を使った述語論理形式
  3. (外部・内部)結合形式
    3パターンで考えてみた。

データの準備

まずデータベースにデータを準備する。 今回は、英語クラスと数学クラスを受講している生徒の情報を記録したテーブルを用意して考える。

-- 英語クラステーブル作成
CREATE TABLE `schemaName`.`english` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

-- データ挿入
insert into english value(1, '一浪'), (2, '二浪'), (3, '三浪');

-- 数学クラステーブル作成
CREATE TABLE `schemaName`.`math` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NULL,
PRIMARY KEY (`id`));

-- データ挿入
insert into math value(1, '三浪'), (2, '四浪'), (3, '五浪');

和集合

英語あるいは数学の少なくとも一方を受けている生徒をみつける

  1. 集合演算子(MySQL は不可能)
  select name from english union select name from math;
  1. 述語論理
不可能...
  1. 外部結合
不可能...

差集合

英語を受講しているが、数学は受講していない生徒をみつける

  1. 集合演算子(MySQL は不可能)
select * from english except math;
  1. 述語論理
select * from english e where not exists(select * from math m where e.name = m.name);
  1. 結合
select * from english e left join math m on e.name = m.name where m.name is null;

積集合

英語と数学両方を受講している生徒をみつける

  1. 集合演算子(MySQL は不可能)
select * from english intersect math
  1. 述語論理
select * from english e where exists(select * from math m where e.name = m.name);
  1. 結合
select e.name from english e inner join math m on e.name = m.name;

まとめ

MySQL は、和集合除いて集合演算子は使えないみたいですね。
その代わりに結合形式や述語論理形式をつかった表現力が必要になったのだろうと思う。

なぜ述語論理形式で集合演算が可能なのかなと考えてみると、existsが 引数に集合をとれることと、さらに相関サブクエリによって2つの集合の要素(行)同士を比較することができるからなのであろうということはちょっとした気付きになりました。

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 の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。