├── 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 | ![](https://motemen.github.io/images/1920.png) 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 | ![](./images/hatena-business.png) 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 |
599 | 600 | 601 |
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 | ![](./images/relation01.png) 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 | ![](./images/relation02.png) 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 | ![inline 240%](https://cdn.profile-image.st-hatena.com/users/hakobe932/profile.gif?1512697682) 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アクセスポイント名に `