├── Hatena-Intern-2018
├── 0-welcome
│ ├── images
│ │ ├── hatena-business.png
│ │ └── logo.png
│ └── slide.md
├── 1-introduction
│ ├── images
│ │ └── logo.png
│ └── slide.md
├── 2-web-application
│ └── slide.md
├── 3-rdbms
│ ├── images
│ │ ├── logo.png
│ │ ├── relation01.png
│ │ └── relation02.png
│ ├── sample.sql
│ └── slide.md
├── 4-api
│ └── slide.md
├── 5-frontend
│ ├── images
│ │ ├── boxmodel.png
│ │ └── logo.png
│ └── slide.md
├── 6-machine-learning
│ ├── images
│ │ └── learning_curve.png
│ ├── slide.md
│ └── slide.pdf
├── 7-machine-learning
│ ├── slide.md
│ └── slide.pdf
├── 8-infra
│ └── slide.pdf
└── README.md
├── README.md
├── database-programming-perl.md
├── database-programming-scala.md
├── database-programming.md
├── database.sql
├── foundation-of-programming-perl.md
├── foundation-of-programming-scala.md
├── images
├── swift-ios-simulator.png
├── swift-xcode-create-project-1.png
├── swift-xcode-create-project-2.png
├── swift-xcode-debugger.png
├── swift-xcode-debugger.psd
├── swift-xcode-project.png
├── swift-xcode-project.psd
├── swift-xcode-storyboard.png
└── swift-xcode-storyboard.psd
├── swift-development-apps.md
├── swift-programming-language.md
├── web-application-development-perl.md
├── web-application-development-scala.md
└── web-application-development.md
/Hatena-Intern-2018/0-welcome/images/hatena-business.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/0-welcome/images/hatena-business.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/0-welcome/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/0-welcome/images/logo.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/0-welcome/slide.md:
--------------------------------------------------------------------------------
1 | # インターンへようこそ
2 |
3 | ---
4 |
5 | # motemen
6 |
7 | * 2008年入社
8 | * うごメモはてなとか作ってました
9 | * CTO/アプリケーションエンジニア
10 | * 学生に打ち込んだこと: 変愚蛮怒とアイドルマスター
11 | * 最近の趣味はスピードキュービングです
12 | * 困ったらとりあえず motemen を呼んでください
13 |
14 | 
15 |
16 | ---
17 |
18 | # はてなインターン
19 |
20 | * 2008年から毎年開催(11年目)
21 | * 志ある学生にウェブサービス開発の経験と成功体験を持ち帰ってもらう
22 | * 業界にはてなのファンを増やす
23 | * 多くがウェブ業界で活躍中
24 | * 一部は はてなで活躍中
25 |
26 | ---
27 |
28 | # はてなインターンで持ち帰ってほしいもの
29 |
30 | * はてなのウェブサービス開発の体験
31 | * エンジニア仲間とのつながり
32 |
33 | ---
34 |
35 | # カリキュラム
36 |
37 | * 前半: ウェブサービス開発の上から下まで
38 | * ウェブプログラミング(Go)
39 | * 機械学習
40 | * インフラ
41 | * サービス企画・運営
42 | * 後半: 実際のチームで開発
43 |
44 | ---
45 |
46 | # 「サービスを営む」ということ
47 |
48 | * 盛りだくさんの講義
49 | * ウェブサービスを営むのに必要な知識は、プログラミングだけではない
50 | * 企画
51 | * 開発
52 | * 運用
53 |
54 | ---
55 |
56 | # はてなのミッション
57 |
58 | 「知る」「つながる」「表現する」で新しい体験を提供し、人の生活を豊かにする
59 |
60 | ---
61 |
62 | 
63 |
64 | ---
65 |
66 | # はてなのエンジニアが大切にしていること
67 |
68 | * プロダクト志向
69 | * コラボレーション
70 | * おもしろさ
71 | * 学びとオープンネス
72 |
73 | ---
74 |
75 | # インターン中の心構え
76 |
77 | * ハマってる時間はないのでがんがん質問しよう
78 | * 講師 or メンター or 隣の人
79 | * 15分同じことしてたら赤信号。自分が訊いたことが他の人にも有用かも
80 | * ハマったことや気付きはどんどん表出させていこう
81 | * Slack or はてなグループ
82 |
83 | ---
84 |
85 | # 一ヶ月間よろしくお願いします!!!
86 |
--------------------------------------------------------------------------------
/Hatena-Intern-2018/1-introduction/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/1-introduction/images/logo.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/1-introduction/slide.md:
--------------------------------------------------------------------------------
1 | # イントロダクション
2 |
3 | ---
4 |
5 | # カリキュラム
6 |
7 | - 前半: 2週間の講義
8 | - ウェブサービスにまつわる諸々の学習
9 | - 後半: 実際のチームで開発
10 | - チームの一員として、いいもの作っていきましょう
11 |
12 | ---
13 |
14 | ## 前半1週目
15 |
16 | - 月: イントロダクション & 環境セットアップ
17 | - 火: シンプルなウェブアプリケーション
18 | - 水: データベース登場
19 | - 木: API の実装
20 | - 金: フロントエンド
21 |
22 | ---
23 |
24 | ## 1日の流れ
25 |
26 | - 10:30ごろ 8Fセミナールーム(ここ)に出社しましょう
27 | - 10:30 〜 11:00 当日の準備やアンケート記入
28 | - 11:00 前日の課題の〆切
29 | - 11:00 〜 13:00 講義
30 | - 13:00 〜 14:00 ランチ
31 | - 14:00 〜 19:00 課題!!!
32 | - この間の何処かで前日の課題の講評があります
33 |
34 | ### 退社前に日報を書こう
35 |
36 | - 出社時間
37 | - 退社時間
38 | - 今日やったこと
39 | - 学んだこと
40 | - 考えたこと
41 | - はまったこと
42 | - 書いたカッコイイコード
43 | - ランチ情報…などなど
44 |
45 | ---
46 |
47 | ## 前半2週目
48 |
49 | - 月: 機械学習
50 | - 火: 機械学習
51 | - 水: インフラ & サービス開発 & コミュニティ
52 | - 木: AWS ハンズオン
53 | - 金: 後半配属に向けた準備
54 |
55 | 課題らしい課題があるのは機械学習まで。がんばりましょう!
56 |
57 | 2週目のうちに、後半配属チームを決定します
58 |
59 | ---
60 |
61 | # 開発環境
62 |
63 | ソフトウェアエンジニアの道具箱。一日中使うので丁寧に整備し、使いこなそう
64 |
65 | ---
66 |
67 | ## OS
68 |
69 | UNIX 環境(macOS or Linux)が基本
70 |
71 | - 開発ツール・ライブラリが揃っている
72 | - アプリケーションサーバに近い環境(はてなでは主に Debian)
73 |
74 | ---
75 |
76 | ## ターミナルとシェル
77 |
78 | ターミナルは Terminal.app か [iTerm2](https://www.iterm2.com/) がオススメ
79 |
80 | OSと人間とのインタフェース。とくにシェルは使いこなせるようになろう
81 |
82 | ---
83 |
84 | `zsh` か `bash` がオススメ。
85 |
86 | 簡単な使いかただけおぼえとこう:
87 |
88 | - `Ctrl-P` `Ctrl-N` でコマンド履歴をたどる
89 | - `Ctrl-R` でインクリメンタル検索
90 | - `Tab` でコマンド補完
91 |
92 | ---
93 |
94 | よく使うコマンド:
95 |
96 | - ファイルから文字列を検索
97 | - `pt` `rg` `git grep` `grep`
98 | - ファイルを検索
99 | - `fzf` `fd` `find`
100 | - コピペ(macOS のみ)
101 | - `pbcopy` `pbpaste`
102 |
103 | メンターがいろいろ教えてくれるかもしれないぞ!!!
104 |
105 | brew install pt fzf fd
106 |
107 | ---
108 |
109 | ## エディタ
110 |
111 | [Visual Studio Code](https://code.visualstudio.com/)
112 |
113 | 社内で共有されているオススメ設定を [VSCode](https://hatenaintern2018.g.hatena.ne.jp/keyword/VSCode) キーワードに書いてます
114 |
115 | brew cask install visual-studio-code
116 |
117 | 適当なディレクトリで `code .` で VSCode をひらけるぞ
118 |
119 | ---
120 |
121 | ## Git
122 |
123 | [Git](https://git-scm.com/)
124 |
125 | 社内の開発は Git ベースです。Git 対応人材になろう
126 |
127 | [サルでもわかるGit入門 〜バージョン管理を使いこなそう〜](https://backlog.com/ja/git-tutorial/)
128 |
129 | brew install git
130 |
131 | ---
132 |
133 | ## Go
134 |
135 | 今回の講義は Docker(後述)内で進めるけど、手元にも環境があったほうが何かと便利。
136 |
137 | brew install go
138 |
139 | ---
140 |
141 | ### $GOPATH を決めよう
142 |
143 | Go 言語であつかうソースコードは決まったディレクトリ以下に配置する必要がある
144 |
145 | 特にこだわりがなければ `~/go` が自分の GOPATH になります
146 |
147 | `go-Intern-Diary` が GOPATH にない場合は `$GOPATH/src/github.com/hatena/go-Intern-Diary` に移動しておこう
148 |
149 | ---
150 |
151 | ## ghq
152 |
153 | 複数のリポジトリを管理してくれるやつです。とりあえず乗っとくと便利
154 |
155 | [ghq: リモートリポジトリのローカルクローンをシンプルに管理する](https://motemen.hatenablog.com/entry/2014/06/01/introducing-ghq)
156 | [おい、peco もいいけど fzf 使えよ](https://qiita.com/b4b4r07/items/9e1bbffb1be70b6ce033)
157 |
158 |
159 |
160 | brew install ghq
161 |
162 | ---
163 |
164 | # 前半課程の進め方
165 |
166 | - [go-Intern-Bookmark](https://github.com/hatena/go-Intern-Bookmark) ... サンプル
167 | - [go-Intern-Diary](https://github.com/hatena/go-Intern-Diary) ... これをつくるよ
168 |
169 | ---
170 |
171 | ## 課題の提出は Pull Request
172 |
173 | 1. 自分の GitHub アカウントに hatena/go-Intern-Diary を fork
174 | 2. 課題ごとに、master からブランチを切って作業開始
175 | - **こまめに commit & push しよう!**
176 | - ビッグバンコミットは修正しづらい & 読みづらくて 🙅
177 | 3. ブランチから master に向けて Pull Request を作る
178 | 4. 講師にレビューしてもらう、大きな問題がなければ master にマージ
179 |
180 | ---
181 |
182 | # 環境構築しよう
183 |
184 | ---
185 |
186 | ## Homebrew
187 |
188 | [brew.sh](https://brew.sh/index_ja)
189 |
190 | macOS 用のパッケージマネージャ。
191 | これまで紹介したものに限らず、便利なものはだいたい `brew` で入ります
192 |
193 | ---
194 |
195 | ## Docker
196 |
197 | [www.docker.com](https://www.docker.com/)
198 |
199 | - コンテナ型の仮想化環境
200 | - 親ホストの環境を汚さずに、アプリケーションのための環境を用意できる
201 | - 前半課程は Docker 上で進めます
202 | - 後半でも、開発環境が Docker 化されているチームは多い
203 |
204 | インストールしよう:
205 |
206 | brew cask install docker
207 |
208 | または https://store.docker.com/editions/community/docker-ce-desktop-mac
209 |
210 | ---
211 |
212 | ## docker-compose
213 |
214 | * 複数の Docker コンテナの生成・起動の管理を簡単にするツール
215 | * 前半課程ではこれ経由で Docker に触れることになります
216 | * 設定の実体は `docker-compose.yaml` というファイル
217 |
218 | ---
219 |
220 | # ウォームアップ: go-Intern-Bookmark を起動してみよう
221 |
222 | ---
223 |
224 | ## docker-compose up
225 |
226 | cd go-Intern-Bookmark
227 | docker-compose up
228 |
229 | ログが流れてくる...
230 |
231 | Starting go-intern-bookmark_db_1 ... done
232 | Starting go-intern-bookmark_node_1 ... done
233 | Starting go-intern-bookmark_app_1 ... done
234 |
235 | このターミナルはログ専用になります。
236 |
237 | 落ち着いたら http://localhost:8000/ にアクセス
238 |
239 | ---
240 |
241 | ## docker-compose ps
242 |
243 | 別のターミナルから(同じディレクトリで):
244 |
245 | docker-compose ps --services
246 |
247 | こうなってますか?
248 |
249 | db
250 | app
251 | node
252 |
253 | "app", "db", "node" の 3 つのサービス(のプロセス)が起動し、これらでひとつのウェブサービス(の開発環境)が形成されている。
254 |
255 | ---
256 |
257 | `docker-compose ps` してみると:
258 |
259 | Name Command State Ports
260 | ----------------------------------------------------------------------------------------
261 | go-intern-bookmark_app_1 ./build/server Up 0.0.0.0:8000->8000/tcp
262 | go-intern-bookmark_db_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp
263 | go-intern-bookmark_node_1 yarn watch Up
264 |
265 | ---
266 |
267 | ## docker-compose exec
268 |
269 | 以下のコマンドを打ってみよう:
270 |
271 | docker-compose exec db mysql
272 |
273 | "db" サービスで `mysql` コマンドを実行する、の意。
274 |
275 | こんなふうに、MySQL のプロンプトが表示されますか?
276 |
277 | Welcome to the MySQL monitor. Commands end with ; or \g.
278 | Your MySQL connection id is 4
279 | Server version: 5.7.22 MySQL Community Server (GPL)
280 |
281 | Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
282 |
283 | Oracle is a registered trademark of Oracle Corporation and/or its
284 | affiliates. Other names may be trademarks of their respective
285 | owners.
286 |
287 | Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
288 |
289 | mysql>
290 |
291 | ここでさらに以下を入力してみよう:
292 |
293 | mysql> SHOW DATABASES;
294 |
295 | こんなふうに、データベースが一覧できましたか?
296 |
297 | +----------------------+
298 | | Database |
299 | +----------------------+
300 | | information_schema |
301 | | intern_bookmark |
302 | | intern_bookmark_test |
303 | | mysql |
304 | | performance_schema |
305 | | sys |
306 | +----------------------+
307 | 6 rows in set (0.01 sec)
308 |
309 | 満足したら `Ctrl-D` でプロンプトを抜けましょう。
310 |
311 | ---
312 |
313 | ## 止める
314 |
315 | * `docker-compose up` したターミナルで `Ctrl-C`、または
316 | * `docker-compose stop`
317 | * もう一度起動したいときはまた `docker-compose up`
318 |
319 | ---
320 |
321 | # 課題: hatena/go-Intern-Diary に PR
322 |
323 | * go-Intern-Diary の自分の master から適当な名前(`day-1` など)でブランチを切る
324 | * `README.md` に適当に追記して `git commit` & `git push`
325 | * GitHub 上で Pull Request を作成する
326 |
--------------------------------------------------------------------------------
/Hatena-Intern-2018/2-web-application/slide.md:
--------------------------------------------------------------------------------
1 | # Webアプリケーション
2 |
3 | ---
4 |
5 | ## 目次
6 |
7 | - HTTP
8 | - Go言語によるWebアプリケーション
9 | - テスト
10 | - 課題
11 |
12 | ---
13 |
14 | # HTTP
15 |
16 | ---
17 |
18 | ## HTTP
19 | URLにアクセスすると降ってくるHTMLをブラウザーが描画する
20 |
21 | ```
22 | $ curl http://example.com
23 |
24 |
25 |
26 | Example Domain
27 | ```
28 |
29 | - URLとは?
30 | - 更新・削除: メソッド・リクエストボディ
31 | - 返ってくるもの: レスポンスボディ, HTML, JSON, XML
32 | - 他の様々な情報: リクエストヘッダ・レスポンスヘッダ
33 |
34 | ---
35 |
36 | ## HTTP
37 |
38 | - Hypertext Transfer Protocol
39 | - WebブラウザとWebサーバとの間の通信プロトコル
40 | - リクエストとレスポンスのデータの形に関する決まりごと
41 |
42 | 参考文献: [Real World HTTP - 歴史とコードに学ぶインターネットとウェブ技術](https://www.google.co.jp/search?q=Real+World+HTTP)
43 |
44 | ---
45 |
46 | ## HTTPの歴史
47 | HTTP/0.9 (1991年) GETのみ・ステータスコードもない
48 |
49 | HTTP/1.0 (1996年5月) メソッド・レスポンスヘッダ・ステータスコード
50 |
51 | HTTP/1.1 (1997年1月) Hostヘッダ・新しいメソッド
52 |
53 | HTTP/2 (2015年5月) 接続の多重化・ヘッダ圧縮・パイプライン化
54 |
55 | ---
56 |
57 | ## RFC
58 | Request For Comment: 主にインターネットのプロトコルやデータフォーマットに関する技術仕様
59 |
60 | HTTP: The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol for distributed, collaborative, hypertext information systems. (RFC7230)
61 |
62 | IETF (The Internet Engineering Task Force) という団体が管理している
63 |
64 | ---
65 |
66 | ## Eメール
67 | RFC822 (1982年8月): メールのプロトコルはHTTPよりも古い
68 |
69 | RFC2822 (2001年4月): 改定
70 |
71 | HTTPプロトコルの元祖
72 |
73 | ヘッダ
74 |
75 | - `From`, `Sender`, `To`, `Received`
76 | - `Message-Id`, `In-Reply-To`
77 | - `MINE-Version`, `User-Agent`, `Content-Type`
78 |
79 | ---
80 | ### Eメールの例
81 |
82 | ```
83 | Delivered-To: sample@example.com # Headers
84 | Received: from ....example.com (....example.com. [...])
85 | by ...
86 | for ;
87 | Mon, 30 Jul 2018 20:48:15 -0700 (PDT)
88 | Received: from ....mailgun.net (....mailgun.net [...]) by ....example.com
89 | ; Tue, 31 Jul 2018 12:48:14 +0900 (JST)
90 | Sender: support@example.com
91 | Date: Tue, 31 Jul 2018 03:48:13 +0000 (UTC)
92 | From: example Support
93 | To: sample@example.com
94 | Message-Id:
95 | Subject: Hello, John.
96 | Mime-Version: 1.0
97 | User-Agent: postal/2.0.2
98 | Content-Type: text/plain; charset=utf-8
99 |
100 | Hey John, # Body
101 | ```
102 |
103 | ヘッダ + 本文 という形はHTTPに引き継がれている
104 |
105 | ---
106 |
107 | ## HTTP概要
108 |
109 | - HTTPリクエスト
110 | - メソッド
111 | - パス
112 | - ヘッダ
113 | - ボディ
114 | - HTTPレスポンス
115 | - ステータスコード
116 | - ヘッダ
117 | - ボディ
118 |
119 | ---
120 |
121 | ### 例
122 | ```
123 | $ curl -v http://example.com/
124 | * Trying 93.184.216.34...
125 | * TCP_NODELAY set
126 | * Connected to example.com (93.184.216.34) port 80 (#0)
127 | > GET / HTTP/1.1
128 | > Host: example.com
129 | > User-Agent: curl/7.54.0
130 | > Accept: */*
131 | >
132 | ```
133 |
134 | ---
135 |
136 | ### 例
137 | ```
138 | < HTTP/1.1 200 OK
139 | < Accept-Ranges: bytes
140 | < Cache-Control: max-age=604800
141 | < Content-Type: text/html
142 | < Date: Sun, 05 Aug 2018 11:21:44 GMT
143 | < Etag: "1541025663"
144 | < Expires: Sun, 12 Aug 2018 11:21:44 GMT
145 | < Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
146 | < Server: ECS (oxr/83C5)
147 | < Vary: Accept-Encoding
148 | < X-Cache: HIT
149 | < Content-Length: 1270
150 | <
151 |
152 |
153 |
154 | Example Domain
155 | ```
156 |
157 | ---
158 |
159 | ## HTTPリクエスト
160 | - メソッド
161 | - パス
162 | - ヘッダ
163 | - ボディ
164 |
165 | ---
166 |
167 | ## URL (RFC1738, 1808)
168 | Uniform Resource Locator: リソースの住所, アドレス
169 |
170 | - 例: [http://www.example.com:80/index.html?id=sample#foo=bar](http://www.example.com:80/index.html?id=sample#foo=bar)
171 | - スキーム: `http`, `https`, `mailto`, `file`, `telnet`
172 | - ホスト名: `www`
173 | - ドメイン名: `example.com`
174 | - ポート番号: `:80` (`http`), `:443` (`https`)
175 | - パス: `index.html`
176 | - クエリ文字列: `?id=sample`
177 | - フラグメント `#foo=bar`
178 |
179 | ---
180 |
181 | ### URLのエンコーディング
182 | - Percent-Encoding
183 | - スペースやUTF-8文字などURLで許されない文字をURLで表現するためのエンコーディング
184 | - 例: https://www.google.com/search?q=%E3%81%BB%E3%81%92%E3%81%BB%E3%81%92
185 | - Punycode: RFC3492
186 | - ドメイン名で使われるエンコーディング
187 | - 例: https://xn--url-883bvcubx104b.com/
188 | - 必ず `xn--` から始まる
189 |
190 | ---
191 |
192 | ### URI (RFC3986)
193 | A Uniform Resource Identifier (URI) is a compact sequence of characters that identifies an abstract or physical resource.
194 |
195 | URL (Uniform Resource Locator)・URN (Uniform Resource Name) を包含する一般的な概念 (URNは名前でURLは住所)
196 |
197 | URLとURIは区別されていたが、W3Cによるともう気にしなくてよい
198 |
199 | - ref: [https://url.spec.whatwg.org](https://url.spec.whatwg.org)
200 | - Go・Python・Node.jsはURL, Ruby・C#はURI
201 |
202 | ---
203 |
204 | ### URIの例 (RFC3986)
205 | - ftp://ftp.is.co.za/rfc/rfc1808.txt
206 | - http://www.ietf.org/rfc/rfc2396.txt
207 | - mailto:John.Doe@example.com
208 | - tel:+1-816-555-1212
209 | - telnet://192.0.2.16:80/
210 | - urn:oasis:names:specification:docbook:dtd:xml:4.1.2 (URN)
211 |
212 | ---
213 |
214 | ## リクエストメソッド (RFC7231)
215 | The request method token is the primary source of *request semantics*; ...
216 |
217 | 同じリソースに対して異なるメソッドを使うことで様々な操作を表すことができる
218 |
219 | - `GET`: リソースを転送する
220 | - `HEAD`: ヘッダのみ要求する
221 | - `POST`: リクエストのペイロードに対してリソース固有の処理を行う
222 | - `PUT`: リクエストのペイロードで対象リソースを置き換える
223 | - `DELETE`: 対象リソースを削除する
224 | - その他: `CONNECT`, `OPTIONS`, `TRACE`
225 |
226 | ---
227 |
228 | ## リクエストヘッダ (RFC7231)
229 | - リクエストのコンテキストの情報
230 | - `Form`, `Referer`, `User-Agent`
231 | - 対象リソースの状態に基づいてリクエストを切り替える
232 | - `If-Match`, `If-Modified-Since`, 304 Not Modified (RFC7232)
233 | - レスポンスのフォーマット
234 | - `Accept-Charset`, `Accept-Encoding`, `Accept-Language`
235 | - 認証情報
236 | - `Authorization` (RFC7235)
237 |
238 | ---
239 |
240 | ## リクエストボディ (RFC7231)
241 | - リクエスト・メッセージ、ペイロード
242 | - データの新規作成・更新
243 | - データ形式
244 | - HTMLフォーム: `multipart/form-data`, `x-www-form-urlencoded`
245 | - JSON: `application/json`
246 | - XML: `application/xml`
247 |
248 | ---
249 |
250 | ## HTTPレスポンス
251 | - ステータスコード
252 | - ヘッダ
253 | - ボディ
254 |
255 | ---
256 |
257 | ## ステータスコード (RFC7231)
258 | レスポンスの意味を表す三桁の数字: 一桁目で分類
259 |
260 | - `1xx` `Informational`: リクエスト受付・処理を続行
261 | - `2xx` `Successful`: リクエスト受理
262 | - `3xx` `Redirection`: リダイレクト
263 | - `4xx` `Client Error`: リクエストに誤り
264 | - `5xx` `Server Error`: サーバーの処理失敗
265 |
266 | ---
267 |
268 | ### 主なステータスコード
269 | - `200`: `OK`
270 | - `303`: `See Other`
271 | - `307`: `Temporary Redirect`
272 | - `400`: `Bad Request`
273 | - `401`: `Unauthorized`
274 | - `403`: `Forbidden`
275 | - `404`: `Not Found`
276 | - `500`: `Internal Server Error`
277 | - `503`: `Service Unavailable`
278 |
279 | ---
280 |
281 | ## レスポンスヘッダ (RFC7231)
282 | サーバーの状態や対象リソースの情報を返す
283 |
284 | - `Age`, `Cache-Control`, `Expires`
285 | - `Location`
286 | - `Etag`, `Last-Modified`
287 | - `Content-Encoding`, `Content-Type`, `Content-Length`
288 | - `Retry-After`
289 | - `Server`
290 |
291 | ---
292 |
293 | ## セキュリティー関連のヘッダ
294 |
295 | - `X-XSS-Protection`: XSSに対するフィルター機能を強制
296 | - `X-Frame-Options`: iframeによりアクセスできないように制御
297 | - `X-Content-Type-Options`: `Content-Type`を無視したスクリプトの実行を抑制
298 | - `Content-Security-Policy`: 実行するスクリプトの制限
299 | - `Strict-Transport-Security`: 次回以降httpsでのリクエストを強制
300 |
301 | ---
302 |
303 | ## レスポンスボディ (RFC7231)
304 | リクエストに対応するリソースの内容や、処理の結果を返す。
305 |
306 | - HTML文章
307 | - JSON
308 | - XML
309 |
310 | ---
311 |
312 | ### HTTPリクエスト
313 | ```
314 | GET / HTTP/1.1 # Request line
315 | Host: example.com # Headers
316 | Accept: */*
317 | ```
318 |
319 | ---
320 |
321 | ### HTTPリクエスト
322 | ```
323 | POST /post HTTP/1.1 # Request line
324 | Host: httpbin.org # Headers
325 | Content-Type: application/json
326 | Content-Length: 21
327 |
328 | { "sample": "hello" } # Body
329 | ```
330 |
331 | ---
332 |
333 | ### HTTPレスポンス
334 |
335 | ```
336 | HTTP/1.1 200 OK # Status line
337 | Cache-Control: max-age=604800 # Headers
338 | Content-Type: text/html
339 | Date: Sun, 05 Aug 2018 12:58:22 GMT
340 | Server: ECS (oxr/8313)
341 | Content-Length: 1270
342 |
343 | # Body
344 |
345 |
346 | Example Domain
347 | ```
348 |
349 | ---
350 |
351 | ## 認証
352 |
353 | ---
354 |
355 | ### HTTPプロトコルは状態を持たない
356 | リクエストとレスポンスの形や解釈の仕方
357 |
358 | 状態を持たないプロトコルの上で、状態を持つように見せかけるにはどうするか
359 |
360 | - キャッシュ (`ETag`, `If-None-Match`, 304 Not Modified)
361 | - 認証
362 |
363 | ---
364 |
365 | ### Basic認証
366 | `user:password`を`base64`して`Authorization`ヘッダに入れてリクエストする
367 |
368 | ```
369 | GET /basic-auth/sample/passwd HTTP/1.1
370 | Host: httpbin.org
371 | Authorization: Basic c2FtcGxlOnBhc3N3ZA==
372 |
373 | HTTP/1.1 200 OK
374 | Connection: keep-alive
375 | Server: gunicorn/19.9.0
376 | Date: Sun, 12 Aug 2018 15:19:38 GMT
377 | Content-Type: application/json
378 | Content-Length: 49
379 |
380 | {
381 | "authenticated": true,
382 | "user": "sample"
383 | }
384 | ```
385 |
386 | ---
387 |
388 | ### Cookie (RFC6265)
389 | 状態を持つセッションを管理できるようにする仕組み。
390 |
391 | - `Set-Cookie` レスポンスヘッダ
392 | - サーバーはこのヘッダに情報を乗せる
393 | - クライアントはこの情報を保存する
394 | - `Expires`, `Max-Age`, `Domain`, `Path`, `Secure`, `HttpOnly` 属性
395 | - `Cookie` リクエストヘッダ
396 | - 同じドメインに対するリクエストに、保存している情報をヘッダとして送信する
397 |
398 | ---
399 |
400 | ### Cookieを使った認証
401 | - ログイン
402 | - ユーザーに紐づくトークンを発行し、`Set-Cookie`で返す
403 | - クライアントはトークンをローカルに保存
404 | - 認証
405 | - `Cookie`のトークンからユーザーを解決する
406 | - ユーザーがなかったりトークンが古かったら認証失敗
407 | - ログアウト
408 | - 値を空、`Expires`を昔にして`Set-Cookie`を返す
409 | - クライアントは該当トークンを消す
410 |
411 | ---
412 |
413 | ### Cookieの注意事項
414 | - HTTPの場合は平文で送受信される
415 | - `Secure` 属性をつけると、HTTPS接続以外では送信しない
416 | - ユーザーが意図的に閲覧・変更・削除できる
417 | - クライアントは`Set-Cookie`を無視してよい
418 | - 最大容量が4KB
419 |
420 | 「Cookieの歴史はwebセキュリティーの歴史」
421 |
422 | ---
423 |
424 | ### JWT (RFC7519)
425 | - JSON Web Token `/dʒɒt/` (ジョット)
426 | - JSONをエンコードしたURL-safeな署名付きのトークン
427 | - `header.payload.signature`
428 | - header: 署名アルゴリズム・トークンタイプの `Base64URL`
429 | - `{ "alg": "HS256", "typ": "JWT" }`
430 | - payload: 独自の情報+クレーム情報 の `Base64URL`
431 | - signature: 改竄されていないことを確かめるための署名
432 |
433 | ---
434 |
435 | ## HTTP/2 (RFC7540, 7541)
436 | レイテンシの改善を目的とし、GoogleのSPDYプロジェクトをもとに仕様が策定された、HTTPのバージョン
437 |
438 | - リクエストとレスポンスの多重化
439 | - ヘッダの圧縮: `HPACK`
440 | - サーバープッシュ
441 | - ストリームの優先順位と重み
442 |
443 | ---
444 |
445 | # Go言語によるWebアプリケーション
446 |
447 | ---
448 |
449 | ## Webアプリケーション
450 |
451 | - リポジトリ層: `repository`
452 | - DBとのデータのやり取り
453 | - サービス層: `service`
454 | - アプリケーションのロジック
455 | - ウェブ層: `web`
456 | - URLルーティング・認証・テンプレートのレンダリング
457 |
458 | ---
459 |
460 | ## リポジトリ層
461 | DBとのデータのやり取り (SQLの詳細は明日)
462 |
463 | ```go
464 | func (r *repository) FindUserByID(id uint64) (*User, error) {
465 | var user model.User
466 | err := r.db.Get(
467 | &user,
468 | `SELECT id,name FROM user WHERE id = ? LIMIT 1`,
469 | id,
470 | )
471 | if err != nil {
472 | if err == sql.ErrNoRows {
473 | return nil, userNotFoundError
474 | }
475 | return nil, err
476 | }
477 | return &user, nil
478 | }
479 | ```
480 |
481 | ---
482 |
483 | ## サービス層
484 | アプリケーションのロジック
485 |
486 | - `App` は `Repository` を持つ
487 | - リポジトリの関数を使って処理を記述する
488 |
489 | ```go
490 | type bookmarkApp struct {
491 | repo repository.Repository
492 | }
493 |
494 | func (app *bookmarkApp) FindUserByID(userID uint64) (*model.User, error) {
495 | return app.repo.FindUserByID(userID)
496 | }
497 | ```
498 |
499 | ---
500 |
501 | ## ウェブ層
502 | - ルーティング
503 | - リクエストボディ
504 | - 認証
505 | - テンプレート
506 |
507 | ---
508 |
509 | ## ルーティング
510 | URLパスに対して `http.Handler` を登録していく
511 |
512 | ```go
513 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
514 | io.WriteString(w, "Hello, world!\n")
515 | })
516 | ```
517 |
518 | - [`github.com/dimfeld/httptreemux`](https://github.com/dimfeld/httptreemux)
519 | - URLの中のパラメータ
520 | - メソッドのサポート: なければ `405 Method Not Allowed`
521 | - パスのグルーピング
522 |
523 | 参考: [https://golang.org/pkg/net/http/](https://golang.org/pkg/net/http/), [https://godoc.org/github.com/dimfeld/httptreemux](https://godoc.org/github.com/dimfeld/httptreemux)
524 |
525 | ---
526 |
527 | ## リクエストボディ
528 | HTMLのフォーム (`multipart/form-data`)
529 |
530 | ```go
531 | func Handler(w http.ResponseWriter, r *http.Request) {
532 | name, password := r.FormValue("name"), r.FormValue("password")
533 | if err := app.CreateNewUser(name, password); err != nil {
534 | http.Error(w, err.Error(), http.StatusInternalServerError)
535 | return
536 | }
537 | }
538 | ```
539 |
540 | ---
541 |
542 | ## リクエストボディ
543 | JSON
544 |
545 | ```go
546 | func Handler(w http.ResponseWriter, r *http.Request) {
547 | var d SomeData
548 | if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
549 | http.Error(w, err.Error(), http.StatusInternalServerError)
550 | return
551 | }
552 | fmt.Printf("%+v\n", d)
553 | }
554 | ```
555 |
556 | ---
557 |
558 | ## 認証: Cookie
559 |
560 | ```go
561 | const sessionKey = "BOOKMARK_SESSION"
562 |
563 | func Login(w http.ResponseWriter, r *http.Request) {
564 | user = ... // name, password を受け取って確認
565 | expiresAt := time.Now().Add(24 * time.Hour)
566 | token := app.CreateNewToken(user.ID, expiresAt)
567 | http.SetCookie(w, &http.Cookie{ Name: sessionKey, Value: token, Expires: expiresAt })
568 | http.Redirect(w, r, "/", http.StatusSeeOther)
569 | }
570 |
571 | func Handler(w http.ResponseWriter, r *http.Request) {
572 | var user *model.User
573 | cookie, err := r.Cookie(sessionKey)
574 | if err == nil && cookie.Value != "" {
575 | user, _ = app.FindUserByToken(cookie.Value)
576 | }
577 | // user: ログイン中のユーザー
578 | }
579 | ```
580 |
581 | ---
582 |
583 | ## HTMLテンプレート
584 | - テンプレートファイルの読み込みとコンパイル
585 | - [go-assets-builder](https://github.com/jessevdk/go-assets-builder)でバイナリー埋め込み
586 | - テンプレートの実行
587 | - テンプレート引数
588 |
589 | 参考: [https://golang.org/pkg/html/template/](https://golang.org/pkg/html/template/)
590 |
591 | ---
592 |
593 | ## テンプレート
594 |
595 | ```html
596 | {{if .User}}
597 | ユーザー名: {{.User.Name}}
598 |
602 | {{else}}
603 | ユーザー登録
604 | ログイン
605 | {{end}}
606 | ```
607 |
608 | 参考: [https://golang.org/pkg/html/template/](https://golang.org/pkg/html/template/)
609 |
610 | ---
611 |
612 | ## ミドルウェア
613 | Webアプリケーションの様々な処理がミドルウェアとして表現できる
614 |
615 | - 認証
616 | - リクエスト・レスポンスのログ
617 | - レスポンスヘッダ
618 |
619 | 玉ねぎの絵が出てくる: [web framework middleware onion](https://www.google.co.jp/search?q=web+framework+middleware+onion&tbm=isch)
620 |
621 | ---
622 |
623 | ### ミドルウェア
624 |
625 | Go言語では `http.Handler` を受け取って `http.Handler` を返す関数
626 |
627 | ```go
628 | type MiddlewareFunc func(http.Handler) http.Handler
629 | ```
630 |
631 | ```go
632 | func loggingMiddleware(next http.Handler) http.Handler {
633 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
634 | start := time.Now()
635 | next.ServeHTTP(w, r)
636 | log.Printf("%s %s took %.2fmsec", r.Method, r.URL.Path,
637 | float64(time.Now().Sub(start).Nanoseconds())/1e6,
638 | )
639 | })
640 | }
641 | ```
642 |
643 | ---
644 |
645 | ## レイヤーの結合
646 | SQLを発行するのはリポジトリ層の責務である。サービス層が直接SQLを発行できないようにするにはどうすればいいだろうか。
647 |
648 | - サービス層はリポジトリの操作を組み合わせて実装したい
649 | - 「サービス」は「リポジトリ」を持っている?
650 | - リポジトリの具体を知りたくない
651 | - 接続先がMySQLかどうか・SQLですらないかもしれない
652 |
653 | → 抽象に依存させよ
654 |
655 | ---
656 |
657 | ### レイヤーの結合
658 |
659 | ```go
660 | // 抽象を公開
661 | type Repository interface {
662 | FindUserByID(id uint64) (*model.User, error)
663 | }
664 |
665 | // 非公開
666 | type repository struct {
667 | db *sqlx.DB
668 | }
669 |
670 | func (r *repository) FindUserByID(id uint64) (*model.User, error) {
671 | }
672 |
673 | // サービスはinterfaceに依存
674 | type bookmarkApp struct {
675 | repo Repository
676 | }
677 | ```
678 |
679 | ---
680 |
681 | # テスト
682 |
683 | ---
684 |
685 | ## テスト
686 |
687 | - 書いたコードが意図した挙動をしているか
688 | - コーナーケースが考慮されているか
689 | - 動いてはいけないケースをきちんと弾いているか
690 | - ライブラリーのバージョンを上げても、アプリケーションの挙動が変わらないこと
691 | - 昨日の自分は赤の他人
692 |
693 | ---
694 |
695 | ### テストで確認すること
696 |
697 | - 正常系
698 | - 正しい条件で正しく動くこと
699 | - 異常系
700 | - おかしな条件で正しくエラーを吐くこと
701 | - 境界条件・極端な条件
702 | - 空の配列、巨大なデータ
703 |
704 | ---
705 |
706 | ### テストよもやま話
707 |
708 | - 機能試験・性能試験
709 | - ユニットテスト・統合テスト
710 | - カバレッジ
711 | - 命令網羅: C0
712 | - 分岐網羅: C1
713 | - 条件網羅: C2
714 | - TDD (テスト駆動開発)
715 | - Table Driven Test: Go言語で推奨される
716 |
717 | 参考: [https://ja.wikipedia.org/wiki/ソフトウェアテスト](https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%83%86%E3%82%B9%E3%83%88)
718 |
719 | ---
720 |
721 | # 課題
722 |
723 | ---
724 |
725 | ## Webアプリケーション
726 | 日記webアプリケーション go-Intern-Diary のユーザー登録・ログイン機能を作ってください。
727 |
728 | - ユーザーに関連するテーブルを作りましょう: `db/`
729 | - リポジトリ層・サービス層を作りましょう: `repository/`, `service/`
730 | - (オプション) サービス層はテストを書きましょう
731 | - ウェブ層をつくりましょう: `web/`
732 | - トップページのテンプレートを作り、ブラウザーでアクセスできるようにしましょう
733 | - ログイン・ログアウト機能を作りましょう
734 | - `/signup` でユーザー登録できる
735 | - ログアウトできる
736 | - `/signin` でログインできる
737 |
738 | go-Intern-Bookmarkのコピペでよいですが、コンパイルできることを確認、コミットしながら進めましょう。
739 |
740 | ---
741 |
742 | ### オプション課題
743 |
744 | - 日記のwebアプリケーションのモデルを考えましょう。
745 | - `X-Frame-Options: DENY` を消すと挙動が変わることを確かめてください。このヘッダをつけていないwebアプリケーションに対する攻撃方法を調べましょう。
746 | - CSRFトークンのinputを消すとどうなるか確認してください。CSRFとは何の略でしょうか。CSRF対策をしていないwebアプリケーションに対する攻撃方法を調べましょう。
747 | - ログインパスワードを安全に保存する方法について調べましょう。bcryptの利点を答えてください。PBKDF2やscryptとの違いを調べましょう。
748 | - セッショントークンをJWTにしてください。JWTが使われているプロトコルについて調べましょう。
749 |
750 | ---
751 |
752 | # 発展研究: HTTP
753 |
754 | ---
755 |
756 | ## HTTPの基礎
757 | - `curl -v -I http://httpbin.org` を試してみよう。
758 | - リクエストメソッド、URL、ヘッダ、レスポンスのステータス、ヘッダを確認しよう。
759 | - `-I` を取って試してみよう。
760 | - サーバーで使われている技術についてなにか分かるだろうか。
761 | - `http://b.hatena.ne.jp` や `http://hatena.ne.jp` にリクエストしてみよう。
762 | - `Content-Length`の値は正しいだろうか。
763 |
764 | ---
765 |
766 | ## メソッド
767 | - `curl -v http://httpbin.org/post` を実行して、レスポンスの意味を理解しよう。
768 | - このURLに対して200が返ってくるようにするにはどうすればいいだろうか。
769 |
770 | ---
771 |
772 | ## リクエストヘッダ
773 | - `http://httpbin.org/headers` にリクエストを送ってみよう。
774 | - `User-Agent`を変更してみよう。
775 |
776 | ---
777 |
778 | ## リクエストボディ
779 | - `http://httpbin.org/post` に `name: John`, `age: 20` というデータを `application/x-www-form-urlencoded` で送信してください。
780 | - `{ "name": "John", "age": 20 }` というJSONを送信してください。
781 | - 適当なテキストファイルを添付して `multipart/form-data` で送信してください。
782 |
783 | ---
784 |
785 | ## Basic認証
786 | - `http://httpbin.org/basic-auth/sample/passwd` にリクエストを送ってみよう。ステータスコードを調べよう。
787 | - このURLに対して200が返ってくるようにするには、どうすればいいだろうか。`curl`の`-u`オプションを使わずにリクエストしてください。
788 |
789 | ---
790 |
791 | ## Cookie
792 | - `curl -v -I http://b.hatena.ne.jp/` を試してみよう。
793 | - `Set-Cookie`に従って`Cookie`ヘッダをつけてリクエストすると何が変わるだろうか。
794 | - Cookieの各属性について調べてみよう。
795 | - Third-party cookieとはどういうものか調べよう。
796 |
797 | ---
798 |
799 | ## リダイレクト
800 | - `http://httpbin.org/redirect/1` にリクエストを送ってみよう。ステータスコードの意味や、リダイレクト先について調べてみよう。
801 | - リダイレクトをフォローするcurlのオプションを調べよう。そのオプションをつけるとどうなるだろうか。
802 | - 二回リダイレクトするとどうなるだろうか。
803 | - `Re-using existing connection!` とはどういう意味だろうか。
804 | - 300番台のステータスコードの違いについて調べよう。
805 | - 308 (RFC7538) の特徴を答えてください。
806 |
807 | ---
808 |
809 | ## ブラウザー・telnet
810 | - ブラウザーを使って、リクエストヘッダ、レスポンスヘッダを確認してみよう。
811 | - ブラウザーでCookieを確認してみよう。
812 | - 304ステータスが返ってくるエンドポイントの仕組みを理解しよう。
813 | - `telnet`コマンドを使って、GETやPOSTなど色々なHTTPリクエストを送ってみよう。
814 | - `Host`ヘッダはなぜ必要なのでしょうか。
815 | - `httpbin.org`の IPアドレスを `dig` や `whois` で調べてください。どういうことがわかりますか。そのIPアドレスに対して `curl` するとどうなりますか。`Host`ヘッダをつけるとどうなりますか。
816 |
817 | ---
818 |
819 | ## HTTP/2
820 | - `example.com` に対してHTTP/1.1とHTTP/2でリクエストしたときの違いを確認しよう。
821 | - ブラウザーでヘッダを見たときになにか気がつくことはあるか。
822 | - ヘッダの圧縮形式 `HPACK` の符号化方式について調べよう。ボディの圧縮に広く使われる `gzip` ではない理由はなぜか。
823 | - ストリーム、メッセージ、フレームの意味を調べよう。
824 | - ストリームの優先順位と重みは何のために必要か。
825 | - HTTP/2によってレイテンシが改善する理由を3つ以上考えよう。
826 | - HTTP/2はHTTP/1.1を置き換えるものではないというのはどういう意味だろうか。
827 | - 仕様が策定された経緯を調べてみよう。
828 |
--------------------------------------------------------------------------------
/Hatena-Intern-2018/3-rdbms/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/3-rdbms/images/logo.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/3-rdbms/images/relation01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/3-rdbms/images/relation01.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/3-rdbms/images/relation02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatena/Hatena-Textbook/0ea9b540d63b80bf52c0e54fd1a7dffe04ef1ca9/Hatena-Intern-2018/3-rdbms/images/relation02.png
--------------------------------------------------------------------------------
/Hatena-Intern-2018/3-rdbms/sample.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS daimyo (
2 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
3 | `name` VARCHAR(32) NOT NULL,
4 | `birthday` DATE NOT NULL,
5 | PRIMARY KEY (id),
6 | UNIQUE KEY (name)
7 | );
8 |
9 | CREATE TABLE IF NOT EXISTS servant (
10 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
11 | `daimyo_id` BIGINT UNSIGNED NOT NULL,
12 | `name` VARCHAR(32) NOT NULL,
13 | `birthday` DATE NOT NULL,
14 | PRIMARY KEY (id),
15 | UNIQUE KEY (name)
16 | );
17 |
18 | -- 注: 生年月日不詳のものは1月1日生まれとしている
19 |
20 | INSERT INTO daimyo (id, name, birthday) VALUES
21 | (1, '織田信長', '1534-06-23'),
22 | (2, '徳川家康', '1543-01-31'),
23 | (3, '武田信玄', '1521-12-01'),
24 | (4, '上杉謙信', '1530-02-18');
25 |
26 | INSERT INTO servant (id, daimyo_id, name, birthday) VALUES
27 | (1, 1, '木下藤吉郎', '1534-06-23'),
28 | (2, 2, '井伊直政', '1561-03-04'),
29 | (3, 1, '前田利家', '1539-01-15'),
30 | (4, 1, '丹羽長秀', '1535-10-16'),
31 | (5, 2, '本多忠勝', '1548-03-17'),
32 | (6, 2, '榊原康政', '1548-01-01'),
33 | (7, 2, '酒井忠次', '1527-01-01'),
34 | (8, 1, '柴田勝家', '1522-01-01'),
35 | (9, 1, '滝川一益', '1525-01-01'),
36 | (10, 2, '石川数正', '1533-01-01'),
37 | (11, 3, '真田昌幸', '1547-01-01'),
38 | (12, 4, '直江景綱', '1509-01-01');
39 |
--------------------------------------------------------------------------------
/Hatena-Intern-2018/3-rdbms/slide.md:
--------------------------------------------------------------------------------
1 | # RDBMS / MySQL
2 |
3 | ----
4 |
5 | # データベース概論
6 |
7 | ----
8 |
9 | ## ところで...
10 |
11 | - SQLを書いたことありますか?
12 | - プログラミング言語から使ったことがありますか?
13 |
14 | ----
15 |
16 | ## データベースとは
17 |
18 | - データ(data) = コンピュータが取り扱う情報
19 | - データベース(database) = データを集めて取り扱いやすくしたもの
20 |
21 | ----
22 |
23 | ## Webサービスとデータベース
24 |
25 | - Webサービスは大量のデータを持つ
26 | - 例えばはてなブログなら, ブログのデータ, 記事のデータ, カテゴリーのデータ...
27 | - アクセスが増えれば増えるほど, データの書き込み/読み込みが増える
28 | - それらを捌きながら, サービスは24時間365日運用したい
29 | - データが消えてしまってはならない
30 | - そのために, データベースを管理するための専用の仕組みを使う
31 | - データベース管理システム (DataBase Management System = DBMS)
32 |
33 | ----
34 |
35 | ## なぜDBMSが必要なのか?
36 |
37 | - データを抽象化し, 効率よく保存できる
38 | - 並列アクセスができる
39 | - データ損失を防ぐ
40 |
41 | ----
42 |
43 | ### データを抽象化し, 効率よく保存できる
44 |
45 | - データを使う人は, データがどのように格納されているか意識しなくても良い
46 | - 用途にあわせて, 最適な構造でデータを保存することができる
47 |
48 | ----
49 |
50 | ### 並列アクセスができる
51 |
52 | - 同時に書き込み/読み込みが発生した時に齟齬が起きないようにできる
53 | - トランザクション/ロック機構
54 |
55 | ----
56 |
57 | ### データ損失を防ぐ
58 |
59 | - 停電でサーバがダウンしたりすることもある
60 | - データを書き込んでいる途中でダウンした場合, 書き込みが中途半端に終わる可能性もある
61 | - そういった際のリカバリーの仕組みを持っている
62 |
63 | ----
64 |
65 | ## 様々なデータベース (1)
66 |
67 | - リレーショナルDBMS (RDBMS)
68 | - MySQL, PostgreSQL, SQLite...
69 | - 今日の講義では, この中のMySQLについて学びます
70 | - カラム指向DBMS
71 | - BigTable, Apache Cassandra, Apache HBase
72 |
73 | ----
74 |
75 | ## 様々なデータベース (2)
76 |
77 | - ドキュメント指向DBMS
78 | - MongoDB, Apache CouchDB, Elasticsearch
79 | - グラフDBMS
80 | - Neo4j
81 | - キーバリューストア (KVS)
82 | - Memcached, Redis, Riak
83 |
84 | ----
85 |
86 | # MySQLとは
87 |
88 | ----
89 |
90 | ## MySQLについて
91 |
92 | - リレーショナルDBMS(RDBMS)の一種
93 | - RDBMS = 関係モデルに基づいたデータベースの一種
94 | - 関係モデルについては後述します
95 | - オープンソースで開発されている
96 | - はてなではMackerel以外のほぼ全てのサービスで使われている
97 |
98 | ----
99 |
100 | ## 関係モデル
101 |
102 | - 関係モデルとは?
103 | - データを関係として表現し, 取り扱うモデルのこと
104 | - ここでの「関係」とは?
105 | - 属性を持った"組(タプル)"の集合で表される
106 | - 関係において, 差, 和, 直積, 射影, 結合などの演算を数学的に定義できる
107 | - 関係は, わかりやすさのために"テーブル(表)"と呼ばれる
108 |
109 | ----
110 |
111 | ### 関係モデルの例
112 |
113 | ```
114 | R: (ID, 名前, 生年月日) = {
115 | (1, 織田信長, 1534-06-23),
116 | (2, 徳川家康, 1543-01-31),
117 | (3, 武田信玄, 1521-12-01),
118 | (4, 上杉謙信, 1530-02-18)
119 | }
120 | ```
121 |
122 | ----
123 |
124 | ### 関係モデルに基づいたデータベース
125 |
126 | - = RDBMS
127 | - データベースは複数の"テーブル(表)"を持つ = 関係
128 | - データは"レコード(列)"で表される = 組(タプル)
129 | - レコードは"カラム(属性)"を持つ
130 | - SQLと呼ばれる言語で, テーブルの定義, テーブルに保存されたデータの参照, テーブルに対する演算などを行う事ができる
131 |
132 | ----
133 |
134 | ### RDBMSにおけるテーブル
135 |
136 | - 例: `daimyo`(大名)テーブル
137 |
138 | | id | name | birthday |
139 | |:---|:---------|:-----------|
140 | | 1 | 織田信長 | 1534-06-23 |
141 | | 2 | 徳川家康 | 1543-01-31 |
142 | | 3 | 武田信玄 | 1521-12-01 |
143 | | 4 | 上杉謙信 | 1530-02-18 |
144 |
145 | ----
146 |
147 | ### RDBMSにおけるテーブル
148 |
149 | - 例: `servant`(家来)テーブル
150 |
151 | | id | daimyo_id | name | birthday |
152 | |:---|:----------|:-----------|:-----------|
153 | | 1 | 1 | 木下藤吉郎 | 1537-03-17 |
154 | | 2 | 2 | 井伊直政 | 1561-03-04 |
155 |
156 | ----
157 |
158 | # SQLとは
159 |
160 | ----
161 |
162 | ## SQLとは
163 |
164 | - 関係データベースに問い合わせ(操作)を行うための言語
165 | - 標準化されていて, ほとんどのRDBMSで利用可能
166 | - データの定義
167 | - データの作成, 読み込み, 更新, 削除
168 |
169 | ----
170 |
171 | ## テーブルの定義
172 |
173 | ----
174 |
175 | ### `daimyo`テーブルなら...
176 |
177 | 次のようなSQLで定義される:
178 |
179 | ```sql
180 | CREATE TABLE daimyo (
181 | id INTEGER NOT NULL AUTO_INCREMENT,
182 | name VARCHAR(32),
183 | birthday DATE,
184 | PRIMARY KEY(id)
185 | );
186 | ```
187 |
188 | ----
189 |
190 | ### `servant`テーブルなら...
191 |
192 | ```sql
193 | CREATE TABLE servant (
194 | id INTEGER NOT NULL AUTO_INCREMENT,
195 | daimyo_id INTEGER,
196 | name VARCHAR(128),
197 | birthday DATE,
198 | PRIMARY KEY(id)
199 | );
200 | ```
201 |
202 | ----
203 |
204 | ### MySQLにおけるデータ型
205 |
206 | - 代表的なもの(よく使うもの)を紹介します:
207 | - 整数型
208 | - 日付型
209 | - 文字列型
210 |
211 | ----
212 |
213 | #### 整数型 (1)
214 |
215 | - 整数型として`TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `BIGINT`が使える
216 | - 右のほう程, 大きい整数値を保存できる
217 | - `UNSIGNED`を付与すると, 符号無しで保存できるようになる(例: `UNSIGNED INT`)
218 | - 桁あふれに気をつける必要がある
219 | - 例えば, `INT`型は`-2147483648`から`2147483647`の範囲の整数を保存できる
220 | - しかし, 21億レコードは案外簡単に到達できる
221 |
222 | ----
223 |
224 | #### 整数型 (2)
225 |
226 | - `id`などは`BIGINT UNSIGNED`にしておくと安全
227 | - `18446744073709551615`(1844京)まで保存できる
228 | - 整数型には`AUTO_INCREMENT`属性を指定することができる
229 | - レコードを保存するとき, 値を指定しなければRDBMSが自動的に採番して値を割り当ててくれる
230 |
231 | ----
232 |
233 | #### 日付型
234 |
235 | - 日付型としては, `DATETIME`や`TIMESTAMP`をよく使います
236 | - `DATETIME`
237 | - フォーマットは`YYYY-MM-DD HH:MM:SS`
238 | - 範囲は`1000-01-01 00:00:00`から`9999-12-31 23:59:59`まで
239 | - `TIMESTAMP`
240 | - フォーマットは`YYYY-MM-DD HH:MM:SS`
241 | - 範囲は`1970-01-01 00:00:01`から`2038-01-19 03:14:07`まで
242 |
243 | ----
244 |
245 | #### 文字列型 (1)
246 |
247 | - 文字列を保存するための型として, `CHAR`, `VARCHAR`をよく使います
248 | - `CHAR`
249 | - 固定長文字列, 0から255文字
250 | - `VARCHAR`
251 | - 可変長文字列, 0から65,535バイト
252 |
253 | ----
254 |
255 | #### 文字列型 (2)
256 |
257 | - 長い文字列を保存するときには`TEXT`が利用できます
258 | - `TEXT`
259 | - 最大65,535バイト
260 | - `MEDIUMTEXT`
261 | - 最長16,777,215バイト
262 | - `LONGTEXT`
263 | - 最長4,294,967,295
264 |
265 | ----
266 |
267 | ### MySQLにおける制約
268 |
269 | - `NOT NULL`制約
270 | - カラムの中身が必ず存在する(`NULL`にならない)ときに付与する
271 | - `NULL` = データが存在しないことを表す値
272 | - `UNIQUE KEY`制約
273 | - カラムの中身がテーブル内で一意(重複しない)ときに付与する
274 | - `DEFAULT`制約
275 | - カラムにデフォルト値を定義したいときに付与する
276 |
277 | ----
278 |
279 | #### データ型と制約を使ったテーブルの定義
280 |
281 | - 先程の`daimyo`テーブルを, データ型と制約を使って定義すると...
282 |
283 | ```sql
284 | CREATE TABLE daimyo (
285 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
286 | `name` VARCHAR(32) NOT NULL,
287 | `birthday` DATE NOT NULL,
288 | PRIMARY KEY (id),
289 | UNIQUE KEY (name)
290 | );
291 | ```
292 |
293 | ----
294 |
295 | ### `PRIMARY KEY`
296 |
297 | - テーブル内でレコードを一意に識別できるカラム
298 | - 1つのテーブルで1つ指定できる / 指定しなくてもよい
299 | - `PRIMARY KEY`は次の制約を満たさないといけない
300 | - 他のレコードと重複してはいけない(`UNIQUE`制約)
301 | - 値がなければならない(`NOT NULL`制約)
302 | - `PRIMARY KEY`はインデックス(後述)としても使える
303 |
304 | ----
305 |
306 | ### テーブルの削除
307 |
308 | - テーブルの削除は`DROP TABLE`でできます
309 | - 但し「削除」なので, テーブルに格納されたデータはすべて消えます
310 |
311 | ```sql
312 | -- daimyoテーブルの削除
313 | DROP TABLE daimyo;
314 | ```
315 |
316 | ----
317 |
318 | ### テーブルの変更
319 |
320 | - 既存のテーブルにカラムを追加したり, 削除したりするのは`ALTER TABLE`でできます
321 | - 現状のテーブルと, 理想のテーブルの差分を記述して, それを適用するイメージ
322 |
323 | ```sql
324 | -- daimyoテーブルにruby(よみがな)テーブルを追加
325 | ALTER TABLE daimyo ADD ruby varchar(32);
326 | ```
327 |
328 | ----
329 |
330 | ## レコードの操作(CRUD)
331 |
332 | - SQLを使って, データベースのデータを操作してみよう
333 | - SQLを使った"操作"のことを"CRUD"とも呼ぶ
334 | - CREATE(レコード作成), READ(レコード読み込み), UPDATE(レコード更新), DELETE(レコード削除)の頭文字
335 |
336 | ----
337 |
338 | ### CREATE
339 |
340 | - レコードを追加する
341 |
342 | ```sql
343 | INSERT INTO daimyo (id, name, birthday) VALUES (5, '毛利元就', '1497-04-16');
344 | INSERT INTO daimyo SET id = 5, name = '毛利元就', birthday = '1497-04-16';
345 | ```
346 |
347 | ----
348 |
349 | ### UPDATE
350 |
351 | - 既に存在するレコードを更新する
352 | - 更新したいレコードは, `WHERE`で指定する
353 |
354 | ```sql
355 | UPDATE servant SET name = '羽柴秀吉' WHERE id = 1;
356 | ```
357 |
358 | ----
359 |
360 | ### DELETE
361 |
362 | - 既に存在するレコードを削除する
363 | - 削除したいレコードは, `WHERE`で指定する
364 |
365 | ```sql
366 | DELETE FROM daimyo WHERE id = 4;
367 | ```
368 |
369 | ----
370 |
371 | ### READ
372 |
373 | - テーブルからレコードを検索する
374 |
375 | ```sql
376 | -- daimyoテーブルからIDが1のレコードを検索
377 | SELECT * FROM daimyo WHERE id = 1;
378 |
379 | -- daimyoテーブルからIDが1ではないレコードを検索
380 | SELECT * FROM daimyo WHERE id != 1;
381 |
382 | -- daimyoテーブルからbirthdayが1530年1月1日以降のレコードを検索
383 | SELECT * FROM daimyo WHERE birthday >= '1530-01-01';
384 | ```
385 |
386 | ----
387 |
388 | ### `WHERE`節
389 |
390 | - READ, UPDATE, DELETEで利用する`WHERE`節について紹介します
391 | - `WHERE`節では, かなり複雑な条件を組み立てることができるので, この講義ではその中でもよく使うものを中心に取り扱うことにします
392 |
393 | ----
394 |
395 | #### 比較
396 |
397 | - SQLにおいて, equalは`=`, not equalは`!=`
398 | - プログラミング言語の数値や文字列のように, `>`, `<`, `>=`, `<=`で比較できる
399 |
400 | ```sql
401 | -- daimyoテーブルからIDが1のレコードを検索
402 | SELECT * FROM daimyo WHERE id = 1;
403 |
404 | -- daimyoテーブルからIDが1ではないレコードを検索
405 | SELECT * FROM daimyo WHERE id != 1;
406 |
407 | -- daimyoテーブルからbirthdayが1530年1月1日以降のレコードを検索
408 | SELECT * FROM daimyo WHERE birthday >= '1530-01-01';
409 | ```
410 |
411 | ----
412 |
413 | #### `AND`/`OR`/`IN`
414 |
415 | - `AND`や`OR`で条件を組み合わせることができる
416 | - `LIKE`は文字列の部分一致(`%`がワイルドカードで, 任意の0文字以上の文字列になる)
417 | - 同じカラムに対する`OR`は, `IN`でまとめて表現することもできる
418 |
419 | ```sql
420 | -- daimyoテーブルで, birthdayが1530年1月1日以降で,
421 | -- nameが織田から始まるレコードを検索
422 | SELECT * FROM daimyo WHERE birthday >= '1530-01-01 AND name LINE '織田%';
423 |
424 | -- daimyoテーブルで, idが1か2か3のレコードを検索
425 | SELECT * FROM daimyo WHERE id = 1 OR id = 2 OR id = 3;
426 |
427 | -- 上のSQLをINを使って表現
428 | SELECT * FROM daimyo WHERE id IN (1, 2, 3);
429 | ```
430 |
431 | ----
432 |
433 | #### `ORDER BY`句
434 |
435 | - 検索結果をソートしたい時に使う
436 | - `DESC`は降順, `ASC`は昇順
437 | - 省略した場合は昇順となる
438 |
439 | ```sql
440 | - daimyoの全レコードを, birthdayの降順で検索
441 | SELECT * FROM daimyo ORDER BY birthday;
442 |
443 | - daimyoの全レコードを, birthdayの昇順で検索
444 | SELECT * FROM daimyo ORDER BY birthday ASC;
445 | ```
446 |
447 | ----
448 |
449 | #### `LIMIT`句 / `OFFSET`句
450 |
451 | - `LIMIT`句は検索する件数を指定できる
452 | - `OFFSET`句は指定した値だけ検索結果を読み飛ばしてくれる
453 |
454 | ```sql
455 | -- servantの全レコードから, birthdayの降順で検索して,
456 | -- 5レコード読み飛ばした上で, 3件の検索結果を得る
457 | SELECT * FROM servant ORDER BY birthday LIMIT 3 OFFSET 5;
458 | ```
459 |
460 | ----
461 |
462 | #### `GROUP BY`
463 |
464 | - `GROUP BY`で, データをグループ化できる
465 | - 次のSQLでは, `servent`テーブルを使って, `daimyo_id`ごとの`servant`の数(= ある大名に, 何人の家臣がいるか)を求めている
466 | - `COUNT(*)`は, その条件に合致するレコードの数を表示する
467 | - `AS`は, レコードに別名を与えることができる(次のSQLでは`COUNT(*)`の結果を`servant_count`という名前で表示するように指定している)
468 |
469 | ```sql
470 | -- servantテーブルから, daimyo_idごとのservant数を求める
471 | SELECT daimyo_id, COUNT(\*) AS servant_count FROM servant GROUP BY daimyo_id;
472 | ```
473 |
474 | ----
475 |
476 | #### `LEFT JOIN`
477 |
478 | - `JOIN`を使うことで, 異なるテーブルに保存されたデータを結合することができる
479 | - 次のSQLでは, `servant`テーブルの`daimyo_id`と, `daimyo`テーブルの`id`を使って結合している
480 | - 結果として, `servant`テーブルにある家臣の名前と, その家臣が紐づく(仕えている)`daimyo`テーブルにある大名の名前が表示される
481 |
482 | ```sql
483 | SELECT servant.name, daimyo.name
484 | FROM servant LEFT JOIN daimyo ON servant.daimyo_id = daimyo.id;
485 | ```
486 |
487 | ----
488 |
489 | ## トランザクション処理
490 |
491 | - トランザクション = 不可分な処理のまとまり
492 | - ある一連の処理をするときに, 全部成功 or 全部失敗(取り消し)にしたい
493 | - 例: 銀行の送金
494 | - AさんからBさんへの10万円の送金は, 次のような2つの処理で実現できる
495 | - (1) Aさんの口座から10万円引く
496 | - (2) Bさんの口座に10万円足す
497 | - もし, (2)の処理だけ失敗してしまうと, Aさんは口座から10万円引かれるだけで終わってしまう
498 |
499 | ----
500 |
501 | ### トランザクションとACID特性
502 |
503 | - トランザクションに必要な4つの要素
504 | - Atomicity(原子性) ... 一連の処理は全部実行されるか, 全部実行されないかを保証
505 | - Consistency(一貫性) ... 一連の処理の結果が整合性を保つことを保証
506 | - Isolation(独立性) ... 一連の処理について, 外部から結果だけが見ることが出来て, 実行中の状態が他へ影響しないことを保証
507 | - Durability(永続性) ... 一連の処理について, 処理が完了した段階で結果が失われず, 永続的なものとして保存されていることを保証
508 |
509 | ----
510 |
511 | # より良い設計のために
512 |
513 | ----
514 |
515 | ## テーブル間のリレーション
516 |
517 | - 異なるテーブルの間で, レコードに関係性を持たせたいときがある
518 | - 一対多の関係性
519 | - 多対多の関係性
520 | - このような関係性を持つレコードは, `JOIN`を使って検索することができる
521 |
522 | ----
523 |
524 | ### 一対多
525 |
526 | ----
527 |
528 | 
529 |
530 | ----
531 |
532 | ### 一対多
533 |
534 | - `country`テーブルは`daimyo_id`カラムを持っている
535 | - これによって, `country`テーブルにあるレコード(国)が, どの`daimyo`に関連しているか(支配しているか), という関係性を示せる
536 | - 一対多なので, 1つの`country`のレコードには, 1つの`daimyo`しか紐付けることができない
537 |
538 |
539 | ----
540 |
541 | ### 多対多
542 |
543 | ----
544 |
545 | 
546 |
547 | ----
548 |
549 | ### 多対多
550 |
551 | - 1つの国(`country`)を, 複数の大名(`daimyo`)が支配している事もあり得る(多対多)
552 | - その時は, `country`と`daimyo`を関連付けるテーブル(ここでは`daimyo_country_relation`)を用意する
553 | - `daimyo_country_relation`は, `daimyo`の`id`と`country`の`id`を保存する
554 | - これによって, 多対多の関係性を示せる
555 |
556 | ----
557 |
558 | ## インデックス
559 |
560 | - テーブルには, インデックス(索引)を作成することができる
561 | - インデックスをうまく準備した上で, それを効率的に使うSQLを作れれば, 高速にデータを検索することができる
562 | - MySQLではB木(B-Tree)というデータ構造が使われる
563 | - デメリットもある
564 | - レコードを追加/更新/削除する時にインデックスを構築するオーバーヘッドが生じる
565 | - インデックスを構築した分だけ, テーブルの容量が増える
566 |
567 | ----
568 |
569 | ### インデックスの例
570 |
571 | - 次の例では, `birthday`カラムについでインデックスを作成している
572 |
573 | ```sql
574 | -- birthdayカラムについてインデックスを作成
575 | CREATE TABLE daimyo (
576 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
577 | `name` VARCHAR(32) NOT NULL,
578 | `birthday` DATE NOT NULL,
579 | PRIMARY KEY (id),
580 | UNIQUE KEY (name),
581 | KEY (birthday)
582 | );
583 | ```
584 |
585 | ----
586 |
587 | ### インデックスの例
588 |
589 | - レコードが追加される度に, `birthday`カラムでソートがされたインデックスが構築される
590 | - そのため, インデックスがない時に比べて, `birthday`カラムを使ったクエリを高速に検索できる
591 | - 例: `birthday`の範囲を指定したクエリ
592 | - `SELECT * FROM daimyo WHERE birthday '1530-01-01' <= birthday && birthday <= '1530-12-31'`
593 |
594 | ----
595 |
596 | # MySQLのパフォーマンス対策
597 |
598 | ----
599 |
600 | ## MySQLのパフォーマンス
601 |
602 | - RDBMSはスケールしにくい
603 | - 複数のサーバーで一貫性/可用性を保ちつつデータを分散するのは難しい
604 | - そのため, データベースはWebサービスを開発/運営する上でボトルネックになりやすい
605 | - うまく対応出来ないとサービスダウンにも繋がる
606 |
607 | ----
608 |
609 | ## EXPLAIN
610 |
611 | - 推測するな, 計測せよ
612 | - パフォーマンス対策をする時, 勘で対処してはいけない
613 | - きちんと計測し, ボトルネックを把握して対処していく必要がある
614 | - MySQLに対する計測には, `EXPLAIN`が有効
615 |
616 | ```sql
617 | -- 先頭に"EXPLAIN"を付けることで, そのクエリについての情報を得ることができる
618 | EXPLAIN SELECT servant.name, daimyo.name
619 | FROM servant LEFT JOIN daimyo ON servant.daimyo_id = daimyo.id;
620 | ```
621 |
622 | ----
623 |
624 | ## パフォーマンス対策のためにできること
625 |
626 | - クエリ数に気をつける
627 | - 1つのクエリで取得できるものは1つのクエリで
628 | - ループの中でクエリを都度発行しない(いわゆる「N+1問題」)
629 | - 不要なクエリは投げない
630 | - 遅くなりがちなクエリに気をつける
631 | - インデックスを使っていない/うまく活用できていないクエリ
632 | - 複雑なJOIN, サブクエリ
633 |
634 | ----
635 |
636 | # 今日の講義で話さなかったこと
637 |
638 | - 時間の都合上, 以下の内容については割愛しました
639 | - サブクエリ
640 | - `DISTINCT`
641 | - `UNION`句
642 | - 外部キー(`FOREIGN KEY`)制約
643 | - トリガー
644 | - DBMSそのものの構築やユーザー管理, 権限など
645 |
646 | ----
647 |
648 | # 課題
649 |
650 | ----
651 |
652 | ## テーブル設計
653 |
654 | - `user`テーブル, `diary`テーブル, `article`テーブルを設計してみよう
655 | - `user`は複数の`diary`を持てるようにしよう
656 | - `diary`は複数の`article`を持てるようにしよう
657 | - 出来上がったら, 一度メンターにレビューしてもらうと良いでしょう
658 |
659 | ----
660 |
661 | ## 機能実装
662 |
663 | - 以下の機能を実装してみましょう
664 | - `diary`を作成する機能
665 | - `diary`に記事を書く機能
666 | - `diary`に書かれた記事を読む機能
667 | - 注: 今後の課題でSPA化するので, UI面を作り込みすぎると手戻りが多くなるので気をつけましょう
668 |
669 | ----
670 |
671 | ### 想定されるエンドポイント
672 |
673 | - 少なくとも, 以下のようなエンドポイントが必要になるでしょう:
674 | - `GET /diaries/new` ... `diary`の新規登録フォーム
675 | - `POST /diaries/new` ... 新規登録フォームのリクエストを受け付ける
676 | - `GET /diaries/:diary_id/new` ... `:diary_id`に対応した`diary`で記事を書くためのフォーム
677 | - `POST /diaries/:diary_id/new` ... 記事を書くためのフォームのリクエストを受け付ける
678 | - `GET /diaries/:diary_id/articles/:article_id` ... 個別の記事を見るためのフォーム
679 |
680 | ----
681 |
682 | ### 機能実装の進め方
683 |
684 | - 一気に実装せず, 1機能ごとに, 次のように作業を分離して進めていくのがオススメです
685 | - モデル層を作る: `model/`
686 | - リポジトリ層/サービス層を作る: `repository/`, `service/`
687 | - Web層を作る: `web/`
688 | - テンプレートを作成して, ブラウザで見れるようにする
689 |
690 | ----
691 |
692 | ## オプション課題
693 |
694 | - 余裕があれば, 追加で機能実装をしてみましょう. 例えば...
695 | - ページング
696 | - コメント機能(`comment`テーブルを設計して...)
697 | - 記事を削除する機能
698 |
--------------------------------------------------------------------------------
/Hatena-Intern-2018/4-api/slide.md:
--------------------------------------------------------------------------------
1 | # 今日のテーマ
2 | - セキュリティ
3 | - APIとGraphQL
4 | - フロントエンド開発環境
5 |
6 | 盛りだくさんだけどがんばろう!
7 |
8 | ---
9 |
10 | # 自己紹介
11 |
12 | - id:hakobe932/@hakobe
13 | - ゲームチーム/チーフエンジニア
14 | - 好きな技術: 設計/マイクロサービス/プログラミング言語
15 | - 大学時代に打ち込んだこと: 田村ゆかり
16 |
17 | ---
18 |
19 | 
20 |
21 | ---
22 |
23 | # セキュリティ
24 |
25 | ---
26 |
27 | # セキュリティ
28 | - セキュリティが守られていないソフトウェアには大きなリスクがある
29 | - ユーザの大切なデータが盗まれたり壊されたり利用不可能に
30 | - 企業の機密情報の漏洩
31 | - Webサイトの脆弱性も格好の標的
32 |
33 | ---
34 |
35 | # 事例
36 |
37 | - [PlayStation Networkの個人情報漏洩](http://cdn.jp.playstation.com/msg/sp_20110427_psn.html)
38 | - 氏名/住所/パスワードなどが漏洩
39 | - クレジットカード情報はおそらく無事
40 | - 原因: [SQLインジェクション - Wikipedia](https://ja.wikipedia.org/wiki/SQL%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)
41 |
42 | ---
43 |
44 | # セキュリティの定義
45 |
46 | 正当な権利をもつ個人や組織が、情報やシステムを意図通りに制御できること
47 |
48 | - 機密性: 許可されたひとだけがアクセスできる
49 | - 完全性: 意図された処理が正しく完了できる
50 | - 可用性: システムが必要なときに利用できる
51 |
52 | これらを脅かす攻撃からシステムを守る必要がある
53 |
54 | ---
55 |
56 | # 脆弱性対策の基本
57 |
58 | 1.根本的対策
59 | - 脆弱性を作り込まない実装をする
60 | 2.保険的対策
61 | - 攻撃による影響を軽減する
62 |
63 | 1を基本に、もしものときに備えて2をやる
64 |
65 | ---
66 |
67 | # 基本的な脆弱性と対策
68 |
69 | - SQLインジェクション
70 | - XSS
71 | - CSRF
72 |
73 | ---
74 |
75 | # SQLインジェクション
76 | - 任意のSQLを実行されてしまう脆弱性
77 | - データを破壊したり、結果を取得して機密情報を盗んだり
78 | - ユーザの入力をそのままSQLに埋め込むことで発生
79 |
80 | ```go
81 | // http://myapi.test/user?user_id=1
82 |
83 | query := "SELECT * FROM user WHERE ID = " + req.FormValue("user_id")
84 | db.Query(query)
85 | // => SELECT * FROM user WHERE ID = 1
86 | ```
87 |
88 | ---
89 | # 入力値にSQLを埋め込める
90 | 任意のクエリを発行可能!
91 |
92 | ```go
93 | // http://myapi.test/user?user_id=1%3BDELETE%20FROM%20user%3B
94 |
95 | query := "SELECT * FROM user WHERE ID = " + req.FormValue("user_id")
96 | db.Query(query)
97 | // => SELECT * FROM user WHERE ID = 1; DELETE FROM user;
98 | ```
99 |
100 |
101 | ---
102 | # 対策: プレースホルダを使う
103 | SQLのドライバに付属する機能
104 |
105 | ```go
106 | query := "SELECT * FROM user WHERE ID = ?"
107 | db.Query(query, 1)
108 | // => SELECT * FROM user WHERE ID = 1
109 |
110 | db.Query(query, "; DELETE FROM user;")
111 | // => SELECT * FROM user WHERE ID = '1; DELETE FROM user';
112 | ```
113 |
114 | みなさんが使ってるsqlxでも同じ
115 |
116 | ---
117 |
118 | # XSS: クロスサイトスクリプティング
119 | - サイト上で任意のスクリプトを実行されてしまう脆弱性
120 | - サイトの内容を書き換えられる
121 | - Cookieを盗まれてセッションハイジャックされる
122 | - ユーザの入力をそのままHTMLやCSSに埋め込むことで発生
123 | - JavaScriptによっても発生するので注意
124 |
125 | ---
126 |
127 | # XSSの例 1
128 |
129 | テンプレート
130 |
131 | ```html
132 | {{define T}}Hello! {{.}}
{{ end}}
133 | ```
134 |
135 | レンダリング
136 |
137 | ```go
138 | t, err := template.New("foo").Parse(tmpl)
139 | m := template.HTML(req.FromValue("message")) // エスケープ不要の印を付ける
140 | t.ExecuteTemplate(out, "T", m)
141 | ```
142 |
143 | ※わざわざ入力値のエスケープを無効にしてる
144 |
145 | ---
146 |
147 | # XSSの例 1
148 |
149 | 値が有効なHTMLとして埋め込まれるのでscriptが実行可能に
150 |
151 | ```
152 | GET /hello?message=%3Cscript%3Ealert(1)%3B%3C%2Fscript%3E
153 | # message=
154 | ```
155 |
156 | ```html
157 | Hello!
158 | ```
159 |
160 |
161 | ---
162 |
163 | # XSSの例 2
164 |
165 | jQueryでHTMLを構築するときなどに発生
166 |
167 | だめな例
168 |
169 | ```javascript
170 | $('' + bookmark.comment +'
')
171 | ```
172 |
173 | 正しい例
174 |
175 | ```javascript
176 | const e = $('')
177 | e.text(bookmark.comment)
178 | ```
179 |
180 | ---
181 | # XSSのデモ
182 |
183 | 背景を変えていたずらできる
184 |
185 | ```
186 |
187 | ```
188 |
189 | 別のサイトに移動させることができる
190 |
191 | ```
192 |
193 | ```
194 |
195 | セッション情報を引き抜ける
196 |
197 | ```
198 |
199 | ```
200 |
201 | ---
202 | # 対策
203 |
204 | - テンプレートへの文字列埋め込み時には必ずエスケープする
205 | - Goのhtml/templateを使えば自動的にやってくれる!
206 | - URLを出力するときは`http://`や`https://`のみを許可
207 | - Goのhtml/templateを使えば自動的にやってくれる!!
208 | - Reactでも無理しなければ自動的にやってくれる!!
209 |
210 | あえてHTMLの入力をゆるしたい場合は慎重なプログラミングが必要
211 |
212 | ---
213 |
214 | # 対策(続き)
215 |
216 | - スタイルシートにも注意 (expreassion())
217 | - 重要なCookieにhttponly属性を付与する
218 | - X-Content-Type-Options: nosniff
219 | - 古いIEでJavaScript以外のものが評価されないように
220 | - X-XSS-Protection: 1; mode=block
221 | - ブラウザの対策機能を有効にする
222 | - Content-Security-Policy ヘッダの活用
223 |
224 | ---
225 |
226 | # Content-Security-Policy ヘッダ
227 |
228 | - ブラウザが読み込んでも良いリソースのポリシーを設定できる
229 | - 指定されたスクリプト以外はインラインやHTML属性のイベントハンドラも無視される
230 | - 参考: [https://developer.mozilla.org/ja/docs/Web/HTTP/CSP](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP)
231 |
232 | ---
233 |
234 | # CSP ヘッダの例
235 |
236 | ```
237 | Content-Security-Policy:
238 | default-src 'self';
239 | img-src *;
240 | media-src media1.com;
241 | ```
242 |
243 | - デフォルトでは元のホストのみを信頼
244 | - 画像はどこでもOK
245 | - 音声や動画はmedia1.comを信頼
246 |
247 | ---
248 | # おまけ: XSS対策の歴史
249 |
250 | ```html
251 | ようこそ [% user.nickname | html %] さん
252 | ```
253 |
254 | - ` | html` がないと、エスケープ**されない**
255 | - 一つでも忘れるとXSSになって危険だった
256 |
257 | ---
258 | # 意識しよう: ユーザの入力値を信じない!
259 | - ディレクトリトラバーサル
260 | - OSコマンドインジェクション
261 | - HTTPヘッダインジェクション
262 |
263 | ユーザの入力を素通しするとこういう脆弱性がやってくる。どのようにエスケープするかは対策するものによる。
264 |
265 | ---
266 |
267 | # CSRF: クロスサイト・リクエスト・フォージェリ
268 |
269 | - 意図せずログイン状態のリクエストを発生させられてしまう脆弱性
270 | - 意図しない投稿/商品購入/退会処理など
271 | - 投稿元のWebサイトをチェックしないことで発生
272 | - デモ
273 |
274 | ---
275 |
276 | # 対策: CSRFトークンを利用する
277 |
278 | - POSTリクエストにユーザごとに秘密のトークンを含める
279 | - HTMLのformのhiddenパラメータやリクエストヘッダに含めることが多い
280 | - サーバ側で投稿された秘密のトークンが正しいかどうかをチェック
281 | - サンプルコードでは[justinas/nosurf](https://github.com/justinas/nosurf)を利用
282 |
283 | ---
284 |
285 | # その他の脆弱性
286 |
287 | - クリックジャッキング
288 | - バッファオーバーフロー
289 | - サイドチャネル攻撃
290 | - [Side-channel attacking browsers through CSS3 features](https://www.evonide.com/side-channel-attacking-browsers-through-css3-features/)
291 |
292 | ---
293 |
294 | # 運用レベルでの対策
295 | - OSやソフトウェアの継続的アップデート
296 | - HTTPSの採用
297 | - 強力な認証方法の採用
298 | - WAFの利用
299 |
300 | ---
301 |
302 | # 参考資料
303 | - [IPA 独立行政法人 情報処理推進機構:情報セキュリティ](https://www.ipa.go.jp/security/)
304 | - [安全なWebサイトの作り方](https://www.ipa.go.jp/security/vuln/websecurity.html)
305 | - [安全なSQLの呼び出し方](https://www.ipa.go.jp/security/vuln/websecurity.html#sql)
306 | - [セキュアプログラミング講座](https://www.ipa.go.jp/security/awareness/vendor/programming/index.html)
307 | - [Japan Vulnerability Note](https://jvn.jp/)
308 |
309 | ---
310 |
311 | # API
312 |
313 | ---
314 |
315 | # API is 何
316 |
317 | * API = Application Programming Interface
318 | * ソフトウェアコンポーネントをプログラムから操作するためのインターフェース
319 | * 例) WebサービスのAPI, データベースのAPI, ライブラリのAPI
320 |
321 | ---
322 |
323 | # 広く公開されたAPI
324 |
325 | - 目的: 特定のサービスのエコシステムの拡大や利便性の向上
326 | - 利用者: 任意の開発者
327 | - 例) Twitter, Facebook, GitHub, はてなブログ
328 |
329 | ---
330 |
331 | # 例) GitHub
332 |
333 | - GitHubの情報を使ったアプリケーションが簡単に作れる
334 | - ルールにしたがって利用する
335 | - アプリケーションをサイトに登録
336 | - アプリケーションごとにリクエスト頻度などの制限がある
337 | - [ドキュメント](https://developer.github.com/v3/)
338 |
339 | ---
340 |
341 | # 例) はてなブログ
342 |
343 | - AtomPub仕様に準拠したAPI
344 | - 記事の取得/新規作成/更新/削除
345 | - [ドキュメント](http://developer.hatena.ne.jp/ja/documents/blog/apis/atom)
346 |
347 | ---
348 |
349 | # 特定のフロントエンドのための専用API
350 | - 目的: 特定のWebサイトで動的なWebページを構築する
351 | - 利用者: Webサイトの開発者
352 | - 特定の機能を実現するための専用の設計
353 |
354 | ---
355 |
356 | # 例) はてなブログの内部API
357 |
358 | - はてなブログのフロントエンドが使う
359 | - 多様な機能/用途に合わせた形式
360 | - ユーザには公開されていない
361 | - 例: [はてなブログの管理画面](http://blog.hatena.ne.jp)
362 |
363 | ---
364 |
365 | # 対象となる開発者が違う
366 | - Large Set of Unknown Developers (LSUDs)
367 | - Small Set of Known Developers (SSKDs)
368 |
369 | 用途によってAPIの性質を決めよう
370 |
371 | [.footer: [The future of API design: The orchestration layer](https://thenextweb.com/dd/2013/12/17/future-api-design-orchestration-layer/) より]
372 |
373 | ---
374 |
375 | # APIの目的に合わせた設計
376 |
377 |
378 | - APIの形式
379 | - 認証方法
380 | - ドキュメンテーション
381 |
382 | ---
383 |
384 | # 課題で作るAPIの場合
385 |
386 | フロントエンド向けの内部APIを作る(SSKD)
387 |
388 | - APIの形式: JavaScriptから使いやすい形式
389 | - 認証方法: HTML配信と同じでも良い
390 | - ドキュメンテーション: 内部向けの簡潔なもの
391 |
392 | ---
393 |
394 | # API形式
395 | フロントエンド向けに適切な形式
396 |
397 | - 基本: REST
398 | - 新技術: GraphQL
399 |
400 | ☆ 最終的にはGraphQLを使います
401 |
402 | ---
403 |
404 | # REST
405 |
406 | ---
407 |
408 | # REST
409 |
410 | - REST = Representational State Transfer
411 | - ステートレス
412 | - リソースをURIで識別する
413 | - HTTPメソッドによる操作
414 | - ハイパーメディア形式によりリソースの参照をたどって扱える
415 |
416 | ---
417 |
418 | | メソッド | 操作 |
419 | | ---- | ---- |
420 | | POST | 新規作成(Create) |
421 | | GET | 取得(Read) |
422 | | PUT | 更新(Update) |
423 | | DELETE | 削除(Delete) |
424 | | PATCH | 部分更新(Patch) |
425 |
426 | ---
427 |
428 | | ステータスコード | 意味 |
429 | | ---- | ---- |
430 | | 200 | OK |
431 | | 201 | Created |
432 | | 204 | No Content |
433 | | 400 | BadRequest |
434 | | 401 | Unauthorized |
435 | | 403 | Forbidden |
436 | | 404 | Not Found |
437 | | 405 | Method Not Allowed |
438 | | 500 | Internal Server Error |
439 |
440 | ---
441 |
442 | # 例) AtomPub API (はてなブログ)
443 |
444 | - RESTに準拠している
445 | - ブログのような記事配信のためのプロトコル
446 | - [RFC5023 Atom Publishing Protocol](http://www.ietf.org/rfc/rfc5023.txt)
447 | - [はてなブログAtomPub](http://developer.hatena.ne.jp/ja/documents/blog/apis/atom)
448 | - 見てみましょう
449 |
450 | ---
451 |
452 | # REST Pros.
453 |
454 | - ステートレスで使いやすい
455 | - ハイパーメディアの思想
456 | - HTTPのセマンティクスは理解しやすい
457 |
458 | # REST Cons.
459 | - 操作的なものなど、リソースという概念で実装するのが難しいものがある
460 | - 例: `/bookmarks/search` 取得と検索は違う
461 | - 厳密な準拠には手間がかかる
462 |
463 | ---
464 |
465 | # REST の現実
466 |
467 | - 完全にRESTの考え方に準拠しているものは少ない
468 | - RESTっぽいもの(=REST-like)が多数
469 | - リソースはURIでアクセスできる
470 | - HTTPメソッド以外の操作もURLで表現
471 | - ハイパーメディアの機能はない
472 | - 安易にRESTという言葉を使うと厳密なREST主義者の人には怒られる
473 |
474 | ---
475 |
476 | # 例) GitHubのAPI
477 | - [GitHub API v3](https://developer.github.com/v3/repos/)
478 | - 見てみよう
479 |
480 | ---
481 |
482 | # GraphQL
483 |
484 | ---
485 |
486 | # REST-like APIの課題
487 |
488 | - データ取得の効率やデータの表現の都合で様々なAPIが林立する
489 | - 特にフロントエンド向けのAPIで..
490 | - 微妙な用途の差により都度APIの実装が必要
491 | - 加えてメンテナンスコストも増大
492 |
493 | ---
494 |
495 | # 最適なデータ取得を行う専用APIの作成
496 |
497 | - `http://myapi.test/bookmarks_with_user`
498 | - `http://myapi.test/bookmarks_with_user_and_entry`
499 | - `http://myapi.test/bookmarks_with_user_and_entry&simple=1`
500 |
501 | ---
502 |
503 | # GraphQL
504 |
505 | - APIのためのクエリ言語とその実装
506 | - クエリを使って入れ子になったデータ構造を柔軟に取得できる
507 | - 用途ごとのAPIは不要に
508 | - 優秀なクライアントライブラリもある ([Apollo](https://www.apollographql.com/))
509 | - 明日紹介
510 |
511 | ---
512 |
513 | # 例) GraphQL リクエスト1
514 |
515 | あるURLのブックマーク一覧を表示する
516 |
517 | ```sh
518 | POST http://localhost:8000/graphql
519 | {
520 | getEntry(entryId: "25699773679927297") {
521 | url
522 | title
523 | bookmarks {
524 | comment
525 | }
526 | }
527 | }
528 | ```
529 |
530 | ---
531 |
532 | # 例) GraphQL レスポンス1
533 |
534 | ```javascript
535 | 200 OK
536 | {
537 | "data": {
538 | "getEntry": {
539 | "url": "https://example.com",
540 | "title": "Example Domain",
541 | "bookmarks": [
542 | {
543 | "comment": "hogehoga",
544 | },
545 | ...
546 | ]
547 | }
548 | }
549 | }
550 | ```
551 |
552 | ---
553 |
554 | # 例) GraphQL リクエスト2
555 |
556 | ユーザ名も表示したい!
557 |
558 | ```sh
559 | POST http://localhost:8000/graphql
560 | {
561 | getEntry(entryId: "25699773679927297") {
562 | url
563 | title
564 | bookmarks {
565 | comment
566 | user {
567 | name
568 | }
569 | }
570 | }
571 | }
572 | ```
573 |
574 | ---
575 |
576 | # 例) GraphQL レスポンス2
577 |
578 | ```javascript
579 | 200 OK
580 | {
581 | "data": {
582 | "getEntry": {
583 | "url": "https://example.com",
584 | "title": "Example Domain",
585 | "bookmarks": [
586 | {
587 | "comment": "hogehoga",
588 | "user": {
589 | "name": "scomb"
590 | }
591 | },
592 | ...
593 | ]
594 | }
595 | }
596 | }
597 | ```
598 |
599 | ---
600 |
601 | # GraphiQL
602 |
603 | - GrqphQLを試すためのUI → [デモ](http://localhost:8000/graphiql)
604 | - Intern-Diaryにも付属しています
605 |
606 | ---
607 |
608 | # GraphQLサーバの実装
609 |
610 | - スキーマを定義する
611 | - クエリによって取得できるデータ構造の定義
612 | - `resolver` を実装する
613 | - クエリに対応するデータを取得する関数
614 | - ライブラリを使ってHTTPサーバに組み込む
615 |
616 | ---
617 |
618 | # スキーマ
619 |
620 | - GraphQLのAPIから取得できるデータ型とその関係(グラフ!)を定義する
621 | - データ型はフィールドを持つ
622 | - 各フィールドの型はGraphQLの仕様で定義されたもの
623 | - or 自分で定義したデータ型
624 | - スキーマに定義されたとおりにフィールドを組み合わせてクエリを発行できる
625 | - `schema` 以下に定義された `query` と `mutation` は特別な意味
626 | - [見てみよう](https://github.com/hatena/go-Intern-Bookmark/blob/master/resolver/schema.graphql)
627 |
628 | ---
629 |
630 |
631 | # データの取得用スキーマ(Query)
632 |
633 | `query` に指定された、typeに定義されたフィールドをトップレベルのクエリとして呼び出せる
634 |
635 | ```
636 | type Query {
637 | visitor(): User!
638 | getUser(userId: ID!): User!
639 | getBookmark(bookmarkId: ID!): Bookmark!
640 | getEntry(entryId: ID!): Entry!
641 | listEntries(): [Entry!]!
642 | }
643 | ```
644 |
645 | - `(...)`の中は引数、`:`以降は返り値の型
646 |
647 | ---
648 |
649 | # Entry
650 |
651 | `getEntry`から得られる`Entry`のデータ構造を定義
652 |
653 | ```
654 | type Entry {
655 | id: ID!
656 | url: String!
657 | title: String!
658 | bookmarks: [Bookmark!]!
659 | }
660 | ```
661 |
662 | `bookmarks` は`Bookmark`のリスト
663 | `!`は`null`を許さないという意味
664 |
665 | ---
666 |
667 | # Bookmark と User
668 |
669 | ```
670 | type Bookmark {
671 | id: ID!
672 | comment: String!
673 | user: User!
674 | entry: Entry!
675 | }
676 | ```
677 |
678 | ```
679 | type User {
680 | id: ID!
681 | name: String!
682 | bookmarks: [Bookmark!]!
683 | }
684 | ```
685 |
686 | ---
687 |
688 | # クエリの例(再掲)
689 |
690 | スキーマにより以下のようなクエリを受け付けてくれるようになる
691 |
692 | ```sh
693 | {
694 | getEntry(entryId: "25699773679927297") {
695 | url
696 | title
697 | bookmarks {
698 | comment
699 | user {
700 | name
701 | }
702 | }
703 | }
704 | }
705 | ```
706 |
707 | ---
708 | # データの取得方法の定義: `resolver`
709 |
710 | - `type` のフィールドごとに `resolve` を定義する
711 | - `getEntry` に対して `GetEntry` 関数を定義
712 | - 先頭が大文字になっていることに注意
713 |
714 | ---
715 |
716 | ```go
717 | func (r *resolver) GetEntry(
718 | ctx context.Context, args struct{ EntryID string }) (*entryResolver, error) {
719 | entryID, err := strconv.ParseUint(args.EntryID, 10, 64)
720 | entry, err := r.app.FindEntryByID(entryID)
721 | if entry == nil {
722 | return nil, errors.New("entry not found")
723 | }
724 | return &entryResolver{entry: entry}, nil
725 | }
726 | ```
727 |
728 | ---
729 |
730 | # `resolver` はデータ構造に合わせて定義する
731 |
732 | - フィールドに対応する値か`resolver`構造体を返す
733 | - 自分で定義した`type`なら`resolver`構造体(例 `entryResolver`)
734 | - GraphQLで定義済みの`type`ならそのまま返す(例 string)
735 | - `type`ごとの`resolver`構造体がデータの取得方法を知っている
736 | - データ構造を再帰的にたどりながら`resolve`することで、データ全体を作ることができる仕組み
737 |
738 | ---
739 |
740 | # `GetEntry`が返す`entryResolver`にさらに`resolver`メソッドを定義
741 |
742 | ```go
743 | type entryResolver struct {
744 | entry *model.Entry
745 | }
746 |
747 | func (e *entryResolver) ID(ctx context.Context) graphql.ID {
748 | return graphql.ID(fmt.Sprint(e.entry.ID))
749 | }
750 |
751 | func (e *entryResolver) URL(ctx context.Context) string {
752 | return e.entry.URL
753 | }
754 |
755 | ...
756 | ```
757 |
758 | ---
759 |
760 | # ライブラリを使って組み込む: `graphql-go`
761 |
762 | - 課題では[graphql-go](https://github.com/graph-gophers/graphql-go)を使う
763 | - スキーマと`resolver`メソッドを定義した構造体を `MustParseSchema` にわたす形
764 |
765 | ```go
766 | graphqlSchema, err := loadGraphQLSchema()
767 | schema := graphql.MustParseSchema(string(graphqlSchema), newResolver(app))
768 | return &relay.Handler{Schema: schema}
769 | ```
770 |
771 | ---
772 |
773 | ```go
774 | type query struct{}
775 |
776 | func (_ *query) Hello() string { return "Hello, world!" }
777 |
778 | func main() {
779 | s := `
780 | schema {
781 | query: Query
782 | }
783 | type Query {
784 | hello: String!
785 | }
786 | `
787 | schema := graphql.MustParseSchema(s, &query{})
788 | http.Handle("/query", &relay.Handler{Schema: schema})
789 | log.Fatal(http.ListenAndServe(":8080", nil))
790 | }
791 | ```
792 |
793 | ```sh
794 | $ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query
795 | ```
796 |
797 |
798 | ---
799 |
800 | # データの変更 (Mutation)
801 | - 更新系のクエリは`Mutation`として定義する習わし
802 | - 基本は`Query`と一緒だがresolver内で作成や更新処理を行って良い
803 |
804 | ```
805 | type Mutation {
806 | createBookmark(url: String!, comment: String!): Bookmark!
807 | deleteBookmark(bookmarkId: ID!): Boolean!
808 | }
809 | ```
810 |
811 | ---
812 |
813 | # GraphQL Pros.
814 |
815 | - 必要なデータをクエリを使って柔軟に取得できる
816 | - Backend For Frontend (= BFF) に最適
817 | - 用途ごとのAPIをはやしたり、たくさんAPIを呼ぶ必要がない
818 | - 機能を作るたびにAPIを変更する必要がない
819 | - スキーマから型情報を生成して利用できる
820 | - クライアントが対応データ型を生成して安全に利用可能
821 |
822 | ---
823 |
824 | # GraphQL Cons.
825 | - `resolver`の書き方によっては特定のクエリで非常にパフォーマンスが悪くなるので、実装に注意が必要
826 | - 第三者が複雑なクエリを発行して攻撃できないように気をつける必要
827 | - クエリの階層の深さや複雑さをチェックする
828 | - [Security and GraphQL Tutorial](https://www.howtographql.com/advanced/4-security/)
829 | - HTTPのセマンティクスを無視しているところがあるので既存の技術が利用しにくい
830 | - キャッシュとか
831 |
832 |
833 | ---
834 |
835 | # パフォーマンスを改善する
836 |
837 | - フィールドに対応する`resolver`を単純に実装するとパフォーマンスが悪い
838 | - DataLoaderという仕組みを使ってデータ取得を遅延評価する方法がある
839 |
840 | ---
841 |
842 | # シンプルな `resolver` の実装
843 |
844 | ```go
845 | func (b *bookmarkResolver) User(ctx context.Context) (*userResolver, error) {
846 | user, _ := b.app.FindUserByID(b.bookmark.UserID)
847 | return &userResolver{user: user}, nil
848 | }
849 | ```
850 |
851 | - bookmarkのuserフィールドを評価するたびに呼び出される
852 | - = bookmarkの数だけSQLが発行される
853 | - SQLの呼び出しを繰り返し行うのは高価
854 |
855 | ---
856 |
857 | # パフォーマンスが良くないGraphQLのクエリ
858 |
859 | ```
860 | {
861 | getEntry(entryId: "97774362819559425") {
862 | bookmarks {
863 | user {
864 | name
865 | }
866 | }
867 | }
868 | }
869 | ```
870 |
871 | - `bookmark`ごとに`user`フィールドが評価される
872 | - `bookmark_resolver`の`User`メソッドが`bookmark`の個数だけ呼び出される
873 | - いわゆるN+1問題
874 |
875 | ---
876 |
877 | # より複雑なクエリ
878 |
879 | ```
880 | {
881 | getEntry(entryId: "97774362819559431") {
882 | bookmarks {
883 | user {
884 | bookmarks {
885 | entry {
886 | bookmarks {
887 | user {
888 | name
889 | }
890 | }
891 | }
892 | user {
893 | name
894 | }
895 | }
896 | }
897 | }
898 | }
899 | }
900 | ```
901 |
902 | - すごくたくさんUserの取得が発生する
903 | - そもそもこういうクエリを許して良いのかという別の話題もある
904 |
905 | ---
906 |
907 | # DataLoader
908 | - データの取得処理を遅延させて、取得処理をまとめて行える仕組み
909 | - 原典は [Facebook dataloader](https://github.com/facebook/dataloader)
910 | - GraphQLの`resolver`のように、取得したいものを一つづつしか指定できないが、パフォーマンスなどの理由で取得は一度にやりたいときに便利
911 | - Intern-Bookmarkでは `loader`パッケージにまとめられている
912 |
913 | ---
914 |
915 | # Load
916 |
917 | ```go
918 | // from loader/user.go
919 | func LoadUser(ctx context.Context, id uint64) (*model.User, error) {
920 | ldr, _ := getLoader(ctx, userLoaderKey)
921 | data, _ := ldr.Load(ctx, userIDKey{id: id})()
922 | return data.(*model.User), nil
923 | }
924 | ```
925 |
926 | - リクエストごとにloaderが作られる
927 | - Loadメソッドで取得したいIDを指定する
928 | - すぐには取得は実行せず他のLoadの呼び出しを待つ
929 | - ある程度Loadが呼び出されてIDが集まってきたら一度に取得する
930 |
931 | ---
932 |
933 | # BatchFunc
934 | - 集まってきたIDの列をもとに一度にデータを取得するための関数
935 | - IDの列と同じ順番で結果を返す
936 | - 取得には、`app.ListUsersByIDs(userIDs)` のように効率よく一度にデータを取得するメソッドを使う
937 | - SQLのWHERE INを使ってデータを一度に取得する
938 | - loaderを作る時に指定する
939 |
940 | ---
941 |
942 | # BatchFuncの例
943 |
944 | ```go
945 | // from loader/user.go
946 | func newUserLoader(app service.BookmarkApp) dataloader.BatchFunc {
947 | return func(ctx context.Context, userIDKeys dataloader.Keys) []*dataloader.Result {
948 | results := make([]*dataloader.Result, len(userIDKeys))
949 | userIDs := make([]uint64, len(userIDKeys))
950 | for i, key := range userIDKeys {
951 | userIDs[i] = key.(userIDKey).id
952 | }
953 | users, _ := app.ListUsersByIDs(userIDs)
954 | for i, userID := range userIDs {
955 | results[i] = &dataloader.Result{Data: nil, Error: nil}
956 | for _, user := range users {
957 | if userID == user.ID {
958 | results[i].Data = user
959 | continue
960 | }
961 | }
962 | if results[i].Data == nil {
963 | results[i].Error = errors.New("user not found")
964 | }
965 | }
966 | return results
967 | }
968 | }
969 | ```
970 |
971 | ---
972 |
973 | # GraphQL参考資料
974 | - [GraphQL](https://graphql.org/)
975 | - [GraphQL Spec](http://facebook.github.io/graphql/June2018/)
976 | - [How to GraphQL](https://www.howtographql.com/)
977 | - [graphql-go](https://github.com/graph-gophers/graphql-go)
978 | - サンプルコード
979 | - [go-graphql-starter](https://github.com/OscarYuen/go-graphql-starter)
980 | - [graphql-go-example](https://github.com/tonyghita/graphql-go-example)
981 |
982 | ---
983 |
984 | # フロントエンド開発環境
985 | - TypeScriptによるフロントエンド開発環境
986 | - モジュールの依存関係解決とバンドルに[Webpack](https://webpack.js.org)を使う
987 |
988 | ---
989 |
990 | # JavaScriptのモジュール
991 |
992 | [ES2015 Module](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import)
993 |
994 | - foo.js:
995 | - `export const foo = function () { return 0; };`
996 | - index.js:
997 | - `import { foo } from 'foo';`
998 |
999 | 少し古いブラウザやIEではまだ動かない
1000 |
1001 | ---
1002 |
1003 | # WebPack
1004 |
1005 | - JSのモジュールの解決とバンドルをしてくれる
1006 | - 様々なフロントエンドリソースを扱う機能(loader)
1007 | - スタイルシートや画像をモジュールのように読み込み
1008 | - JS読み込み時にTypeScriptにコンパイルする
1009 | - 便利な機能いろいろ
1010 |
1011 | ---
1012 |
1013 | # 設定の例
1014 |
1015 | ```javascript
1016 | module.exports = {
1017 | mode: "development",
1018 | devtool: "inline-source-map",
1019 | entry: "./src/index.ts", // このファイルが起点
1020 | output: {
1021 | filename: "index.js" // このファイルが出てくる
1022 | },
1023 | resolve: {
1024 | extensions: [".ts", ".tsx", ".js"]
1025 | },
1026 | module: {
1027 | rules: [
1028 | { test: /\.tsx?$/, loader: "ts-loader" } // .ts や .tsx ファイルがts-loaderにより処理される
1029 | ]
1030 | }
1031 | };
1032 | ```
1033 |
1034 | ---
1035 |
1036 | # 課題: セキュリティ
1037 |
1038 | - オプション課題
1039 | - `
560 | ```
561 |
562 | ## 対策
563 |
564 | - 前述のとおり、出力時に適切なエスケープをすること
565 | - Twirl は自動的にエスケープしてくれるので今回の場合は何もしなくて良い
566 | - `Html()` を使うと明示的にでエスケープをなくせる
567 | - 何らかの理由で html タグを動的に出力したいときに使う
568 | - 使った場合は注意が必要 -> script タグなどが入らないように!
569 |
570 | ## バリデーション
571 |
572 | Scalatraには、 [Commands](http://www.scalatra.org/2.4/guides/formats/commands.html) というユーザーの入力値をケースクラスにマッピングしたり、バリデーションを行なったりする機能があります。
573 |
574 | また、標準ではありませんが、 [scalatra-forms](https://github.com/takezoe/scalatra-forms) というPlay FrameworkのForm機能と似たような使い勝手のFormライブラリもあります。Scalatraのコミッターの方のライブラリなので、クオリティの心配はないでしょう。こちらをユーザー入力チェックのために使っても良いかもしれません。
575 |
576 | ## 課題3
577 |
578 | - CLI版 Intern-Diary を Web アプリケーションにして下さい
579 |
580 | ### (必須)記事の表示
581 |
582 | ブラウザですでに書かれたdiaryの記事を読めるように
583 |
584 | - ブラウザで読めるように
585 | - テンプレートをちゃんと使って
586 | - 設計を意識しよう
587 | - 良いURI設計をしてみよう
588 | - ページャを実装
589 | - OFFSET / LIMIT と ?page=? というクエリパラメータを使います
590 | - 明日課題に繋がるので必須です
591 |
592 | ### (必須)記事作成/編集/削除
593 |
594 | - ブラウザで書けるように
595 | - ブラウザで更新できるように
596 | - ブラウザで削除できるように
597 |
598 | ### (オプション)追加機能
599 |
600 | 以下の様な追加機能をできる限り実装してみてください。
601 |
602 | 例)
603 |
604 | - 認証 (Hatena/Twitter OAuth)
605 | - フィードを吐く (Atom, RSS)
606 | - デザイン
607 | - 管理画面
608 | - いろいろ貼り付け機能
609 | - その他自分で思いついたものがあれば
610 |
611 | ### 注意
612 |
613 | - 全然分からなかったらすぐに人に聞きましょう
614 |
615 | ## 参考:ユーザ認証層
616 |
617 | - 以下のように複数のアプリケーションをmountできる
618 | - 認証処理の実装自体はHatenaOAuth.scala参照のこと
619 |
620 | ```Scala
621 | // src/main/scala/ScalatraBootstrap.scala
622 | import internbookmark._
623 | import jp.ne.hatena.intern.scalatra.HatenaOAuth
624 | import org.scalatra._
625 | import javax.servlet.ServletContext
626 | import internbookmark.service.Context
627 |
628 | class ScalatraBootstrap extends LifeCycle {
629 | override def init(context: ServletContext): Unit = {
630 | Context.setup("db.default")
631 | context.mount(new internbookmark.web.BookmarkWeb, "/*")
632 | context.mount(new HatenaOAuth, "/auth")
633 | }
634 |
635 | override def destroy(context: ServletContext): Unit = {
636 | Context.destroy()
637 | }
638 | }
639 | ```
640 |
641 | ## 参考資料
642 |
643 | - http://scalatra.org/2.4/guides/
644 | - https://www.playframework.com/documentation/2.3.x/ScalaTemplates
645 |
646 | 
この 作品 は クリエイティブ・コモンズ 表示 - 非営利 - 継承 2.1 日本 ライセンスの下に提供されています。
647 |
--------------------------------------------------------------------------------
/web-application-development.md:
--------------------------------------------------------------------------------
1 | # Web開発の基礎
2 |
3 | 目次
4 |
5 | - HTTPとURI
6 | - HTTP
7 | - リクエストとレスポンスの例
8 | - その中でも
9 | - ステータスコード
10 | - 代表的なステータスコード
11 | - URI
12 | - 良いURIとは
13 | - 良いURIの恩恵
14 | - HTTPとの関係
15 | - ここまでのまとめ
16 | - Webアプリケーション概説
17 | - Webアプリケーションの基本
18 | - Webアプリケーションの構成要素
19 | - Webアプリケーションの動作
20 | - 最もシンプルな図
21 | - サーバとアプリケーションを分離した図
22 | - WAFとWebアプリケーション処理を分離した図
23 | - ここまでのまとめ
24 | - MVC
25 | - MVC
26 | - WebアプリケーションのMVC
27 | - ここまでのまとめ
28 |
29 | # HTTPとURI
30 |
31 | - Webアプリに入る前のウォーミングアップです
32 | - 知ってる人は復習で
33 | - Webの基本になる2つの技術
34 | - HTTP
35 | - URI
36 |
37 |
38 | ## HTTP
39 |
40 | - HTTP (Hypertext Transfer Protocol)
41 | - 中身はテキストで書かれたヘッダと(あれば)ボディ
42 | - リクエストとレスポンス
43 |
44 |
45 | ## リクエストとレスポンスの例
46 |
47 | curl -v を使うと中身が見られます。
48 |
49 | ```
50 | curl -v http://hatenablog.com/
51 | ```
52 |
53 | リクエスト
54 | ```
55 | > GET / HTTP/1.1
56 | > User-Agent: curl/7.35.0
57 | > Host: hatenablog.com
58 | > Accept: */*
59 | ```
60 |
61 | レスポンス
62 | ```
63 | < HTTP/1.1 200 OK
64 | < Cache-Control: private
65 | < Content-Type: text/html; charset=utf-8
66 | < Date: Fri, 17 Jul 2015 10:03:42 GMT
67 | < P3P: CP="OTI CUR OUR BUS STA"
68 | < Server: nginx
69 | < Vary: Accept-Encoding
70 | < Vary: Accept-Language, Cookie, User-Agent
71 | < X-Content-Type-Options: nosniff
72 | < X-Dispatch: Hatena::Epic::Global::Index#index
73 | < X-Frame-Options: DENY
74 | < X-Page-Cache: hit
75 | < X-Revision: b4418f9710e3db5110634da7c553c907
76 | < X-Runtime: 0.026343
77 | < X-XSS-Protection: 1
78 | < transfer-encoding: chunked
79 | < Connection: keep-alive
80 | <
81 |
82 |
356 | 今日も元気でした
357 |
358 | ...
359 | ```
360 |
361 | という html を出力するとします。これで A さんはブログを書き、その内容を他の人達に共有することができました。
362 | ここで B さんがこのブログサービスに目を付けました。B さんは「<script>alert('XSS')</script>」という内容で投稿しました。この B さんのブログを見た人には
363 |
364 | ```html
365 | ...
366 |
367 |
368 |
369 | ...
370 | ```
371 |
372 | という html が出力されます。これは有効な html ですので、ブラウザは script タグを解釈して alert を実行します。B さんのブログを見に行った人には、ブログサービス側が意図していないアラートで `'XSS'` が表示されるということです。
373 | B さんにはいたずら心はありましたが悪意があるわけではなかったので、よくわからないアラートが表示されるだけで済みました。もし悪意があれば cookie をいじったり勝手に通信を行ったり、JavaScript で可能な操作ができてしまいます。その結果、ユーザーに意図しない行動をとらせることが出来てしまう可能性があります。
374 |
375 | - 根本的な対策
376 | - 出力時に適切なエスケープをすること
377 | - テンプレートエンジンによっては、自動的にエスケープしてくれるので何もしなくて良い
378 | - ただし、明示的にエスケープをしなくした場合は注意が必要
379 |
380 | ### 何が外部由来の入力か
381 |
382 | 「そりゃフォーム入力とかでしょ」
383 |
384 | **他にもある!!**
385 |
386 | - 周囲のWifiアクセスポイント一覧を出すWebサイト
387 | - Wifiアクセスポイント名に `