読者です 読者をやめる 読者になる 読者になる

akiyoko blog

akiyoko の IT技術系ブログです

Mezzanine の本番設定 〜AWS 環境構築から運用設定まで〜(その1:AWS 環境構築)

Mezzanine AWS Ubuntu

こんにちは、akiyoko です。

Mezzanine は Python製の WordPress風フルスタックCMSフレームワークですが、個人的にブログサイト(将来的には ECサイトを増設予定)を本番運用するために、昨年12月頃から調査をしてきました。

akiyoko.hatenablog.jp


それ以来いろいろと企画やら調整を地道に進めてきましたが、この 7月にようやく本番運用に漕ぎ着けることができました。

それを記念して(?)AWS の初期設定から Mezzanine の本番デプロイ、ちょっとした運用設定までの記録を一旦ここでまとめておきたいと思います。

かなり長くなるので、

  • その1:AWS 環境構築
  • その2:Mezzanine テーマのカスタマイズ
  • その3:Mezzanine の本番デプロイ
  • その4:Mezzanine の運用設定

の 4本に記事を分けました。

今回はその 1本目、「その1:AWS 環境構築」について説明します。


AWS 環境構築として実施した内容としては、

  • IAM 設定(ルートアカウント・admin ユーザの設定)
  • VPC 設定(1ネットワーク+ 1サブネット)
  • EC2 設定(Ubuntu 14.04 LTS インスタンスの起動)
  • Amazon SES 設定(ドメイン認証、送信制限の解除申請)

となります。



 

1. IAM の初期設定

基本方針として、AWS ルートアカウントは原則使用せず、AdministratorAccess 権限を持たせた管理者ユーザー「admin」を新たに作成して利用することとします。

なお、ルートアカウントは、MFA(二段階認証)でセキュリティを強化しておきます。


(参考)


 

1. 1. ルートアカウントに MFA 導入

1.1.1. スマホに仮想MFAアプリケーションをインストール

まず、Android に

というアプリをインストールします。


なお、仮想MFA アプリケーションの一覧はこちらです。
私は Android ユーザなので Android を使っていますが、その他のスマホを使っている場合は適宜別のアプリを検討してください。

f:id:akiyoko:20160829014611p:plain

Android Google AuthenticatorAuthy 2 段階認証
iPhone Google Authenticator
Windows Phone Authenticator
Blackberry Google Authenticator

Multi-Factor Authentication より)



(参考)AWS MFAの設定 - Qiita


 

1.1.2. AWS Management Console での設定

[IAM] > [Dashboard] から、[Security Status] > [Activate MFA on your root account] > [Manage MFA] を選択します。

f:id:akiyoko:20160829014934p:plain

[A virtual MFA device](仮想 MFA デバイス)を選択。

f:id:akiyoko:20160829015326p:plain

[Don't show me this dialog again] を選択して次へ。

f:id:akiyoko:20160829015853p:plain


以下の QRコードを 1.1.1. でインストールした「Google 認証システム」アプリでスキャンして、アプリをルートアカウントと紐付けます。

f:id:akiyoko:20160829020107p:plain


これで、AWS ルートアカウントでのログイン時にアプリで払い出されるワンタイムトークンが必要になるため、不正ログインに対するセキュリティが向上します。

 

1.2. パスワードポリシーの変更

[Dashboard] から、[Security Status] > [Manage Password Policy] をクリックします。

f:id:akiyoko:20160829020329p:plain


ここで、パスワードポリシーを

  • パスワードの最小長:8
  • 英大文字、英小文字、数字の組み合わせ
  • ユーザーにパスワードの変更を許可

とします。

有効期間、再利用禁止などのポリシーは今回設定していませんが、ニーズがあり次第、適宜設定することとします。

f:id:akiyoko:20160829020557p:plain


 

1.3. IAMユーザ、IAMグループの管理

 

1.3.1. IAMユーザ作成

管理者ユーザ「admin」を作成します。

f:id:akiyoko:20160829022915p:plain
f:id:akiyoko:20160829022936p:plain

アクセスキーを CSV(credentials.csv)でダウンロードしておきます。
f:id:akiyoko:20160829023157p:plain

User Name,Access Key Id,Secret Access Key
"admin",AKIxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


次に、作成したユーザの [Security Credentials](認証情報)タブから、自動作成パスワードを払い出して設定します。

[Manage Password]をクリック。
f:id:akiyoko:20160829023522p:plain

「Assign an auto-generated password」をチェックして、[Apply]をクリック。
f:id:akiyoko:20160829023547p:plain

念のため、パスワードを CSV(admin_pass_credentials.csv)でダウンロードしておきます。
f:id:akiyoko:20160829023702p:plain

User Name,Password,Direct Signin Link
"admin",xxxxxxxxxxxx,https://53xxxxxxxxxx.signin.aws.amazon.com/console


なお、adminユーザには二段階認証は設定しません。 *1


 

1.3.2. IAMグループ作成

adminユーザのための管理者グループ「admin」を作成します。

f:id:akiyoko:20160829023957p:plain

完全な管理者アクセス権限(AdministratorAccess)ポリシーをアタッチします。

f:id:akiyoko:20160829024245p:plain
f:id:akiyoko:20160829024306p:plain

次に、admin グループに admin ユーザを追加します。

作成したグループを選択して、「Add Users to Group」をクリック。
f:id:akiyoko:20160829024412p:plain

追加したいユーザ(ここでは「admin」)を選択して、「Add Users」をクリック。
f:id:akiyoko:20160829024528p:plain
f:id:akiyoko:20160829024556p:plain


 

1.3.3. admin で再ログイン

以下、adminユーザで操作します。

一旦ログアウトし、

https://{AWS Account ID}.signin.aws.amazon.com/console

から、admin ユーザでログインします。なお、「AWS Account ID」は、AWSアカウントに紐づく 12桁の数字で、My Account ページの[Accout Settings]からも確認できます。


ログインしたら、リージョンを「Tokyo」にしておきます。
f:id:akiyoko:20160829024729p:plain


 

2. VPC の初期設定

以前に書いた 「AWS体験ハンズオン ~セキュア&スケーラブルウェブサービス構築~」に参加してきました - akiyoko blog で実現したような、

  • 別アベイラビリティゾーンに Webサーバを冗長構成して、ELB を使ってロードバランシング
  • プライベートなサブネットに RDS を配置(且つ、別アベイラビリティゾーンにレプリケーション)

は行わず、最小限の構成にしています。


今回構築する構成の最終形は、このようになります。
f:id:akiyoko:20160911110053p:plain


(参考)0から始めるAWS入門①:VPC編 - Qiita

 

2.1. VPC の作成

CIDR が「10.0.0.0/16」の VPC を作成します。

[VPC] > [Your VPCs] から、「Create VPC」ボタンをクリック。

f:id:akiyoko:20160830001821p:plain

以下を設定します。

Name tag P1
CIDR block 10.0.0.0/16
Tenancy Default

f:id:akiyoko:20160830001850p:plain


【注意】
このままの設定では、あとでインスタンスを起動して sudo を実行した際に、下記のエラーが出るので(コマンド自体は実行されているっぽい)、DNS Hostnames の設定を「Yes」に変更します。(すでに EC2インスタンスを起動していた場合には、再起動が必要です。)

$ sudo ls
sudo: unable to resolve host ip-10-0-0-80

f:id:akiyoko:20160830001913p:plain
f:id:akiyoko:20160830001930p:plain

(参考)amazon web services - AWS error - sudo: unable to resolve host ip-10-0-xx-xx - Stack Overflow


 

2.2. Subnet の作成

CIDR が「10.0.0.0/24」のサブネットを作成します。

[Subnets] から、「Create Subnet」ボタンをクリック。

f:id:akiyoko:20160830001951p:plain

以下を設定します。

Name tag P1 Public #1
VPC P1
Availability Zone ap-notrtheast-1a
CIDR block 10.0.0.0/24

f:id:akiyoko:20160830002016p:plain


 

2.3. Internet Gateway の作成

インターネットゲートウェイを作成します。

[Internet Gateways] から、「Create Internet Gateway」ボタンをクリック。

f:id:akiyoko:20160830002038p:plain

名前を「P1 Public #1」に設定。
f:id:akiyoko:20160830002101p:plain

作成した Internet Gateway を選択して「Attach to VPC」ボタンをクリックし、作成した VPC を選択して、Internet Gateway を VPC に紐付けます。

f:id:akiyoko:20160830002124p:plain
f:id:akiyoko:20160830002146p:plain

[Route Tables] から、作成した VPC の Route Table を選択して、Routes を編集します。

f:id:akiyoko:20160830002204p:plain

以下を設定します。

Destination 0.0.0.0./0
Target 作成した Internet Gateway を選択

f:id:akiyoko:20160830002223p:plain
f:id:akiyoko:20160830002254p:plain


これで VPC の設定は完了です。


3. EC2 インスタンスの起動

3.1. SSH鍵の作成

まず、SSH鍵を作成します。

[EC2] > [Key Pairs] から、「Create Key Pair」をクリック。

f:id:akiyoko:20160910223728p:plain

キーペア名は、「aws_p1」としました。
f:id:akiyoko:20160910223833p:plain

作成すると、自動的に鍵がダウンロードされます。
f:id:akiyoko:20160910223853p:plain


ダウンロードした SSH鍵を .ssh 以下に配置します。
(以下の操作は Mac を想定しています。)

$ mv ~/Downloads/aws_p1.pem ~/.ssh/
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/aws_p1.pem
$ ls -l@ ~/.ssh/aws_p1.pem
-rw-------@ 1 akiyoko  staff  1696  6  5 17:35 /Users/akiyoko/.ssh/aws_p1.pem
    com.apple.metadata:kMDItemWhereFroms     199
    com.apple.quarantine      68
$ xattr -d com.apple.metadata:kMDItemWhereFroms ~/.ssh/aws_p1.pem
$ xattr -d com.apple.quarantine ~/.ssh/aws_p1.pem
$ ls -l@ ~/.ssh/aws_p1.pem
-rw-------  1 akiyoko  staff  1696  6  5 17:35 /Users/akiyoko/.ssh/aws_p1.pem

ここで、ダウンロードした際によく分かんない attribute(Extended Attributes と言うものらしい)が付いてしまっているので、削除しています。

(参考)EA (Extended Attributes) の消し方


 

3.2. EC2インスタンス用の Security Group 作成

EC2インスタンスに付与する Security Group を作成します。

f:id:akiyoko:20160910223918p:plain

以下を設定します。

Security group name P1 Webserver #1
Description P1 Webserver #1
VPC P1
Type Protocol Port Range Source
SSH TCP 22 My IP
HTTP TCP 80 My IP
HTTPS TCP 443 My IP

f:id:akiyoko:20160910223941p:plain


 

3.3. EC2 インスタンス用の IAM Role の作成

EC2インスタンス作成時に IAM Role を設定して起動するため、先に作成しておきます。

f:id:akiyoko:20160910224102p:plain

IAM Role の名前は「ec2-prod」(本番 EC2インスタンス用の Role という意味)としておきます。
f:id:akiyoko:20160910224124p:plain

「Role Type」は「Amazon EC2」を選択。
f:id:akiyoko:20160910224151p:plain

完全な管理者アクセス権限(AdministratorAccess)ポリシーを選択します。
f:id:akiyoko:20160910224226p:plain

「Create Policy」をクリックして、IAM Role の作成は完了です。
f:id:akiyoko:20160910224252p:plain


 

3.4. EC2インスタンスの起動

EC2インスタンスを起動します。

f:id:akiyoko:20160910224329p:plain

「Ubuntu Server 14.04 LTS」を選択。
f:id:akiyoko:20160910224351p:plain

インスタンスタイプは(必要に応じていつでもスケールアップできるので、最初は)「t2.micro」でよいでしょう。
f:id:akiyoko:20160910224416p:plain

以下を設定します。

Network P1
Subnet P1 Public #1
IAM role ec2-prod
Enable termination protection 「Protect against accidental termination」にチェック

f:id:akiyoko:20160910224442p:plain

f:id:akiyoko:20160910224508p:plain

インスタンス名を「P1 Webserver #1」とします。
f:id:akiyoko:20160910224525p:plain

3.2. で作成した Security Group を設定します。
f:id:akiyoko:20160910224552p:plain

f:id:akiyoko:20160910224616p:plain

3.1. で作成した SSH鍵を設定して、インスタンスを起動します。
f:id:akiyoko:20160910224643p:plain


 

3.5. Elastic IP の紐付け

最後に、起動した EC2インスタンスに Elastic IP を紐付けます。

[Elastic IPs]>[Allocate New Address]をクリック。
f:id:akiyoko:20160910224724p:plain

「Yes, Allocate」をクリックして、Elastic IP を払い出します。
f:id:akiyoko:20160910224751p:plain

払い出した Elastic IP を選択し、[Actions]>[Associate Address]をクリック。
f:id:akiyoko:20160910224815p:plain

3.4. で起動したインスタンスを選択して「Associate」をクリックし、Elastic IP を紐付けます。
f:id:akiyoko:20160910224835p:plain


f:id:akiyoko:20160910224945p:plain


ここで念のため、疎通確認しておきます。

$ ssh -i ~/.ssh/aws_p1.pem ubuntu@52.xx.xx.xx



 

4. SES の設定

最後に、メールの設定です。
なおここで、AWS 外のレジストラ(ここでは お名前.com)で独自ドメイン「akixxxx.com」を管理していることを前提しています。


まず最初に、Amazon SES は Tokyo リージョンでは対応していないため *2、「Oregon」リージョンに移動します。

f:id:akiyoko:20160910225033p:plain


 

4.1. ドメイン認証

[Domain] > [Verify a New Domain]でドメインを認証します。
f:id:akiyoko:20160910225132p:plain

Domain に「akixxxx.com」を指定します。
f:id:akiyoko:20160910225203p:plain

送信用の TXTレコードと受信用の MXレコードが表示されます。
f:id:akiyoko:20160910225231p:plain


 

4.2. 送信用 TXTレコードと受信用 MXレコード設定

レジストラ(お名前.com)上でドメインNavi にログインし、上記の送信用の TXTレコードと受信用の MXレコードを設定します。


[ドメイン設定] > [ネームサーバーの設定]>[DNS関連機能の設定]から、該当ドメインをチェックして「次へ進む」をクリック。
f:id:akiyoko:20160910225320p:plain

[DNSレコード設定を利用する]>[設定する]をクリック。
f:id:akiyoko:20160910225355p:plain

以下を設定します。

ホスト名 TYPE TTL VALUE 優先 状態
_amazonses.akixxxx.com TXT 3600 (AWS Management Console の Domain Verification Record の値を貼り付け) 有効
akixxxx.com MX 3600 _amazonses.akixxxx.com 10 有効

なお、MXレコードの優先度は「10」にします(そのまま貼り付けるとダメ。「10」と「inbound-smtp.us-west-2.amazonaws.com」に分割する)。

f:id:akiyoko:20160910225430p:plain

f:id:akiyoko:20160910225513p:plain

f:id:akiyoko:20160910225552p:plain



2時間ほどすると、「Amazon SES Domain Verification SUCCESS」というメールが来ます。
f:id:akiyoko:20160910225630p:plain

ドメインのステータスが「verified」になっていることが確認できます。
f:id:akiyoko:20160910225710p:plain


 

4.3. SES 送信制限の解除申請

ステータスが「verified」になったら、「Request a Sending Limit Increase」の申請をします。

f:id:akiyoko:20160910225743p:plain


申請内容は以下の通り。

Regarding(内容) Service Limit Increase(サービス制限の増加)
Limit Type(制限タイプ) SES Sending Limits(SES送信制限)
Region US West (Oregon)
Limit Desired Daily Sending Quota(希望する一日あたりの送信クォータ)
New limit value 10000
Mail Type(メールの種類) System Notifications(システム通知)
Website URL (メール送信を使用するシステムのURLを指定)
My email-sending complies with the AWS Service Terms and AUP(私は AWS サービス利用規約と AUP に準拠してメールを送信します) Yes
I only send to recipients who have specifically requested my mail(私は明確にリクエストされた受信者にのみメールを送信します) Yes
I have a process to handle bounces and complaints(バウンスや苦情を処理するプロセスがあります) Yes
Use Case Description(申請理由の説明) アカウント登録やパスワード変更など、システムを利用するユーザへの通知に利用します。メール送信は基本的にシステムからの自動送信となり、ユーザ自身が登録したメールアドレス宛に送信するため、バウンスはほとんど発生しません。予想されるユーザ数は当面のところ、○○程度と見込んでいます。(あくまでサンプルですので、状況に合わせて適宜書き直してください。)
Support Language 日本語
Contact method Web(しか選択できません)

f:id:akiyoko:20160910225819p:plain

f:id:akiyoko:20160910225857p:plain


6時間くらい後に承認メールが届きました。
f:id:akiyoko:20160910230541p:plain

Congratulations! After reviewing your case, we have increased your sending quota to 50,000 messages per day and your maximum send rate to 14 messages per second in AWS Region US West (Oregon). Your account has also been moved out of the sandbox, so you no longer need to verify recipient addresses.

AWS Management Console 上はこのようになっています。
f:id:akiyoko:20160910230618p:plain


(参考)Amazon SESによるメール送信環境の構築と実践 | Developers.IO


まとめ

今回、AWS 環境構築として、

  • IAM 設定(ルートアカウント・admin ユーザの設定)
  • VPC 設定(1ネットワーク+ 1サブネット)
  • EC2 設定(Ubuntu 14.04 LTS インスタンスの起動)
  • Amazon SES 設定(ドメイン認証、送信制限の解除申請)

を実施した内容を記載しました。

次回は、Mezzanine 本番設定の第二弾として、「その2:Mezzanine テーマのカスタマイズ」について記載します。


 

参考本

AWS 関連で比較的新しくて良さげな本を紹介します。

Amazon Web Services徹底活用ガイド(日経BP Next ICT選書)

Amazon Web Services徹底活用ガイド(日経BP Next ICT選書)

Amazon Web Services 定番業務システム12パターン 設計ガイド

Amazon Web Services 定番業務システム12パターン 設計ガイド

*1:というか、1台のスマホにつき紐付けられるユーザは 1つだけなのかな??

*2:「米国西部(オレゴン)」のほか、「米国東部(バージニア北部)」「欧州(アイルランド)」でも SES を利用できますが、今回は日本から物理的に一番近そうな米国西部を選択しました。(参考)https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/regions.html

GitHub の Wiki に画像を貼り付ける一番簡単な方法(Wiki リポジトリを clone しないバージョン)

GitHub Git

GitHub の Wiki に画像を貼り付けようとして画像をドラッグ&ドロップすると、
f:id:akiyoko:20160830005915p:plain
こうなって、画像を貼り付けできませんよね?


「じゃあ、どうやって GitHub Wiki に画像を貼り付けるの?」とググると、たいてい、

GitHub の Wiki をローカルに clone して、
貼り付けたい画像ファイルを add して、commit して push してから、

![Link Text](https://github.com/akiyoko/foo/wiki/images/bar.png "Title")

という形式で Markdown を記述


などというやり方が出てきますが、何だかよく分からないし面倒臭そうですよね(実際、面倒臭いです・・)。


実は、それよりもずっと簡単な方法で GitHub Wiki に画像を貼り付ける方法があるのです。


今回はその方法を説明します。


 

方法

1. GitHub Issue を開く

まず、適当な GitHub Issue を開きます。

f:id:akiyoko:20160830004006p:plain

2. Issue に貼り付けたい画像をドラッグ&ドロップ

開いた Issue に、貼り付けたい画像をドラッグ&ドロップすると・・

f:id:akiyoko:20160830004320p:plain

画像を貼り付けるためのタグが生成されます。

f:id:akiyoko:20160830004515p:plain

3. GitHub Wiki に貼り付ける

2. で生成されたタグを GitHub Wiki にコピペすれば OK です。

f:id:akiyoko:20160830004704p:plain
f:id:akiyoko:20160830004729p:plain

簡単ですね!


 

まとめ

GitHub の Wiki に画像を貼り付けるには、GitHub Issue に画像をドラッグ&ドロップして生成されたタグをコピペするだけでオッケーです。


それにしても、どうして GitHub の Wiki はドラッグ&ドロップで画像をアップロードできないのでしょうね??


Git 関連本

Gitが、おもしろいほどわかる基本の使い方33〈バージョン管理、SourceTree、Bitbucket〉

Gitが、おもしろいほどわかる基本の使い方33〈バージョン管理、SourceTree、Bitbucket〉

Gitポケットリファレンス

Gitポケットリファレンス

【PayPal 決済まとめ】PayPal の決済サービスが分かりにくいので 画面遷移パターンごとに使える決済サービス・API を整理してみた

決済 PayPal

タイトル通りなのですが、ドキュメントが古かったりとっ散らかっていたりするためか、数多くある PayPal の決済サービスのどれが使えてどれが使えないかが分かりにくかったので整理してみました。特に、日本では使えない決済サービスもあったりするので、それを明確にしたかったというのが今回の動機です。そして今回は、単に決済サービスの一覧を列挙するだけでは面白くないので、画面遷移のユースケース(パターン)ごとに使える決済サービス・API を整理をしてみることにしました。


まず、PayPal の API 体系について簡単に説明すると、データ伝搬方式として NVP(Name-Value Pair)形式あるいは SOAP形式が利用できる「NVP/SOAP APIs*1 という API が古くからあり、まだまだ現役で使われています 。それに対して、2013年頃から新しい「Payments REST API」が提供され、最近では「Braintree v.zero」という最新の API が整備されつつあります。「Braintree v.zero」についてはまだサンドボックスとなっていますが、現時点での日本での対応状況も含めてできるだけ最新の情報をまとめていこうと思います。


なお今回は、通常の Webサイトでの決済(モバイル決済やアプリ内課金ではなく)についてのみまとめてあります。



 

1. 自サイト完結型

PayPal サイトを経由しない唯一のパターンです。
決済のバックエンドプロセスを PayPal が担ってくれます。

「Direct Credit Card Payments」という決済サービスを使用することでこの画面遷移パターンが実現できるのですが、残念ながら日本では利用不可。

画面遷移パターン

f:id:akiyoko:20160822012151p:plain
Introducing Direct Payment - PayPal Developer の図を元に作成)

① ショッピングカート画面(「購入手続きに進む」ボタン)
    ↓
② 支払い方法選択画面 *2
    ↓
③ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
④ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Direct Credit Card Payments NVP/SOAP APIs UKのみ *3 。Website Payments Pro (WPP) 契約が必要 DoDirectPayment
Direct Credit Card Payments Payments REST API 同上

 

メリット
  • PayPal アカウントが無くてもクレジットカードが利用可能(購入者が PayPal を意識することはない)
  • 自サイト内で決済処理が完結するため、離脱が少なくなる
デメリット
  • 日本では利用不可。現在利用できるのが UK のみに限定されていて、近いうちに日本で使えるようになる可能性はかなり低いと思われる



 

2. iframe による埋め込み型

一見すると、「1. 自サイト内完結型」のように PayPal サイトを経由しないように見えるパターン。iframe を使って PayPal サイトの画面を自サイトに埋め込むことで、この画面遷移パターンを実現できます。

iframe で PayPal サイトを埋め込むには、「ウェブペイメントプラス」という有料サービスの契約が必要。

画面遷移パターン

f:id:akiyoko:20160822012210p:plain
https://www.paypalobjects.com/webstatic/en_GB/developer/docs/pdf/hostedsolution_uk.pdf P.12「How Hosted Solution Works」の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「購入手続きに進む」ボタン)
    ↓
② (iframe の PayPal サイト内)支払い方法選択画面(「今すぐ支払う」ボタン)  *4
    ↓
③ (iframe の PayPal サイト内)決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
ウェブペイメントプラス 日本企業, JPY 利用可。ウェブペイメントプラス契約が必要 「ウェブペイメントプラス」は US などでは提供していないサービス。UK など一部の地域で提供されている「Website Payments Pro Hosted Solution」に相当する

 

メリット
  • iframe内で決済処理がされる(見た目上リダイレクトされない)ため、離脱防止になる
  • ウェブペイメントプラスを利用した場合、自サイトと違和感の無いようにカスタマイズした決済画面を PayPal 側にホスティングさせることができる *5。これにより購入者の不安を軽減することができる
  • PayPal アカウントが無くてもクレジットカードが利用可能(購入者が PayPal を意識することはない)
デメリット
  • ウェブペイメントプラスは、基本手数料のほかに月額手数料 3,000円 が別途かかる。*6


 

3. PayPal サイトへのリダイレクト + 通知型

リダイレクト先の PayPal サイト内で決済を完了させ、自サイト側のリスナーで PayPal からの通知(IPN or PDT)を受け取るのがポイントとなる画面遷移パターン。

いわゆる「ウェブペイメントスタンダード(PayPal Payments Standard)」を使う方法と言えばこのパターンになります。

画面遷移パターン

f:id:akiyoko:20160822182429p:plain
Introducing IPN - PayPal Developer の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
  (リダイレクト)
    ↓
② (PayPal サイト内)ログイン画面(「ログイン」ボタン)
    ↓
③ (PayPal サイト内)最終確認画面(「今すぐ支払う」ボタン)
    ↓
④ (PayPal サイト内)決済完了画面(「ショッピングサイトに戻る」ボタン)
    ↓
  (リダイレクト)
    ↓
⑤ 決済完了通知を受け取る


ちなみに ②の画面では、購入者が PayPal アカウントを持っていなくても、オプションのクレジットカード情報の入力フォームからそのまま決済処理をそのまま進めることもできます。また、クレジットカード情報の入力フォームを PayPal ログインに優先させてデフォルト表示することもできるようです。

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Payment Buttons + Notification PayPal Button Creation ToolInstant Payment Notification (or Payment Data Transfer) 日本企業, JPY 利用可 PayPal Button Creation Tool は、PayPay サイト上で事前に作成した PayPal ボタンの HTMLコードをコピー&ペーストして使うことができる、「PayPal Payments Standard」の一機能
Payment Buttons + Notification Button Manager APIInstant Payment Notification (or Payment Data Transfer) 日本企業, JPY 利用可 API の種類としては、いずれも NVP/SOAP APIs に分類される

 
なお、通知の種類である Instant Payment Notification (IPN) と Payment Data Transfer (PDT) の違いは、IPN が非同期通知、PDT が同期通知となる点。また、IPN は何度も通知されるのに対し、PDT は通知が一度きりで Confirmation のタイミングだけというデメリットがあるので、PayPal としては IPN を使うことを推奨してます。

(参考)

 

メリット
  • 実装が簡単(後述)
  • 「支払いボタン」作成ツールを使えば、生成された HTMLタグをコピー&ペーストするだけで、Payment Buttons の機能を利用可能
デメリット
  • PayPal サイトにリダイレクトされるので、ある程度の離脱が発生することを許容しないといけない
  • 「実装が簡単」という触れ込みだが、複数サービスを組み合わせる必要があるのと、通知が受け取れなかった場合の異常系を実装するのが手間になる
  • セキュリティ的にあまりよろしくないらしい(*7)が、暗号化ボタンを使えば大丈夫なはず??


 

4. PayPal サイトへのリダイレクト型

リダイレクト先の PayPal サイト内で決済を完了させず、自サイト側の最終確認画面で決裁を完了させる画面遷移パターン。

画面遷移パターン

f:id:akiyoko:20160822182454p:plain
Express Checkout with In-Context integration guide - PayPal Developer の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
  (リダイレクト)
    ↓
② (PayPal サイト内)ログイン画面(「ログイン」ボタン)
    ↓
③ (PayPal サイト内)支払承認画面(「支払いに同意」ボタン)
    ↓
  (リダイレクト)
    ↓
④ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
⑤ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Express Checkout NVP/SOAP APIs 日本での利用可 *8 SetExpressCheckout, GetExpressCheckoutDetails, DoExpressCheckoutPayment を使う
Stored Credit Card Payments / PayPal Account Payments Payments REST API 日本での利用可 登録済みのクレジットカードで支払う場合は Stored Credit Card Payments、PayPalアカウントで支払う場合は PayPal Account Payments を使う

 

メリット
  • Express Checkout は利用できる国・地域が多い
  • Express Checkout は事例やドキュメントが比較的豊富
デメリット
  • PayPal サイトにリダイレクトされるので、ある程度の離脱が発生することを許容しないといけない


なお、Express Checkout 利用時のリダイレクトURLのパラメータを変更することで、「3. PayPal サイトへのリダイレクト + 通知型」と同じように、PayPal サイト上で決裁を完了させることも可能です。

(参考)Customizing Express Checkout - PayPal Developer




 

5. ポップアップウィンドウ型(In-context Window)

小さなポップアップを立ち上げ、その内部で PayPal サイトを表示させる、新しいタイプの画面遷移パターン。

画面遷移パターン

f:id:akiyoko:20160816235832p:plain
NVP/SOAP Integration - PayPal Developer の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
② (In-context ウィンドウ内)ログイン画面(「ログイン」ボタン)
    ↓
③ (In-context ウィンドウ内)支払承認画面(「支払いに同意」ボタン)
    ↓
④ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
⑤ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Braintree v.zero SDK 日本での利用不可 サンドボックスで検証
In-context Express Checkout NVP/SOAP APIs 日本企業, JPY 利用不可 *9

 

メリット
  • ポップアップウィンドウ内で決済処理がされるため、離脱防止になる
デメリット
  • Braintree は、比較的新しいサービスで日本ではまだ利用不可(将来的に使えるようになるか不明だが、いろんな決済手段を透過的に利用できるので期待大)


表中に示したように、NVP/SOAP APIs の利用条件としては公式ドキュメントでは「日本企業, JPY 利用不可」となっていますが、PayPal の中の人に聞いたところもうすでに利用可能だと言っていました。

akiyoko.hatenablog.jp



 

まとめ

販売先のメインが日本ということであれば WebPay の API を使うのが一番簡単らしいのですが、越境EC だと利用規約的に NG らしいので、PayPal を使わざるを得ないという状況がまだまだあるように思います。

そしていざ PayPal を使うとして最初につまずくのが、数多くある PayPal のうちどの決済サービス・API を使えばいいのか全然分からなくてお手上げになるという問題です。
現在日本で実現できる画面遷移パターンは、「1. 自サイト内完結型」を除く、

  • 2. iframe による埋め込み型
  • 3. PayPal サイトへのリダイレクト + 通知型
  • 4. PayPal サイトへのリダイレクト型
  • 5. ポップアップウィンドウ型(In-context Window)

ですので、その中から実現したい画面遷移パターンに合わせて、決済サービス・API を選択すれば良いということになります。


なお、機能ごとに使える地域と通貨のまとめが開発者用のページにまとまっていて、こちらのページは必見です。
PayPal Product Availability — By Country - PayPal Developer



また、期待大の最新API の Braintree についてですが、今年の 3〜4月あたりに Sandbox を使ってみたときにはまだまだ使い物にはならなかったという印象でしたが、6月に PayPal の中の人に確認したところ、近い将来使えるようになるということでした。こちらについては今後要検証です。


このまとめ記事が、何かの手助けになれば幸いです。
ただし、自分でも試していないサービスが多いので、間違っている情報がありましたら教えていただけると助かります。


関連本

電子決済ビジネス 銀行を超えるサービスが出現する

電子決済ビジネス 銀行を超えるサービスが出現する

カード決済業務のすべて―ペイメントサービスの仕組みとルール

カード決済業務のすべて―ペイメントサービスの仕組みとルール

クレジットのすべてがわかる!  図解カードビジネスの実務

クレジットのすべてがわかる! 図解カードビジネスの実務

*1:少し古いドキュメントでは「Classic API」と呼ばれるていたのですが、最新の開発者用ドキュメントでは「NVP/SOAP APIs」という名称に統一されたようです。開発者用ドキュメント上ではまだ結構「Classic API」という名称が混在しているようです。。

*2:支払い方法として「PayPal」を選択させることもできるが、今回は省略

*3:https://developer.paypal.com/docs/classic/api/#website-payments-pro

*4:支払い方法として「PayPal」を選択させることもできるが、今回は省略

*5:iframe で埋め込まずにそちらにリダイレクトすることも可能

*6:

*7:PayPalのWeb Paymetn StandardとExpress Checkoutの違いが理解できないあなたへ | 高橋文樹.com

*8:https://developer.paypal.com/docs/integration/direct/rest-api-payment-country-currency-support/

*9:https://developer.paypal.com/docs/classic/express-checkout/in-context/#features-not-supported-by-in-context-express-checkout

「何となくJavaScriptを書いていた人が一歩先に進むための本」と「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」の二冊を読んでトドメに「Effective JavaScript」を読んだら長年のモヤモヤがスッキリして JavaScript 中級者にステップアップできた件

JavaScript

これまでずっとサーバサイドをメインでやってきたとは言え、JavaScript に触れる機会も少なくなかったのですが、正直なところ何度やってもコツが掴めないというか、「JavaScript って独特な言語だなあ」というモヤモヤとした苦手意識がありました。


少し前に、「何となくJavaScriptを書いていた人が一歩先に進むための本」(以下「一歩先」)という本をたまたま Kindle Store で見かけたのをきっかけに、同じ著者の続編「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」(以下「もう一歩先」)を続けて読み、さらに積ん読してあった「Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方」と合わせて、三冊の JavaScript 本を一気に読んでみたら、これまでのモヤモヤがかなりスッキリして何だか JavaScript の初心者を卒業できたような気がしたので、忘れないうちにメモを残しておきたいと思います。



何となくJavaScriptを書いていた人が一歩先に進むための本

何となくJavaScriptを書いていた人が一歩先に進むための本

「一歩先」「もう一歩先」の二冊は薄くてサクサク読めるので非常にオススメです。それぞれ値段も安いですし。
実戦で今すぐ使える知識が多く載っているのが「一歩先」、ES2015 と JavaScript で一番やっかい(?)な this について詳しく学びたければ「もう一歩先」というイメージでしょうか。


Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方

JavaScript 中級者になるためのポイントが詰まっている本。気になるところだけ読んでもいいし、最初から読み進めて「難しいな」と感じた時点でストップしてもいい。項目ごとに「憶えておくべきポイント」としてまとめが書かれているのが素晴らしいです。
なおこの本では、ES2015 については触れられていません(ES5 を前提)。



目次


 

ES5 と ES2015(ES6)の対応状況

ES5(ECMAScript 第5版)の主要ブラウザの対応状況は、
ECMAScript 5 compatibility table
となっており、モダンブラウザであればほぼ確実に動くと見てよい。


一方、次期バージョンの ES2015(通称 ES6)は、Chrome, Firefox, Edge 以外はまだまだ未対応のものが多く、Android, iOS も壊滅的。
ECMAScript 6 compatibility table

少なくとも現時点ではES2015を「そのまま」現場に持ち込むことは現実的とは言えないでしょう。


http://analogic.jp/es2015_introduction/

という昨年10月頃からあまり変わっていない状況。

そんな ES2015 を実践投入するには、「Babel」などのトランスパイラ(ES2015 で書かれたコードを ES5 のコードへ変換する仕組み)を使う。


 

プリミティブ型

JavaScript には、

  • boolean
  • number
  • string
  • null
  • undefined

のプリミティブ型が存在し、通常は「値そのもの」を示す。

number, string, boolean にはラッパーオブジェクトが存在し、場合によっては一時的にオブジェクトとして振る舞うことがあるが、null, undefined は常にプリミティブ型となる。

var n = 1;
console.log(n.toString());  // "1"


 

関数

JavaScript では、次のいずれかの方法で関数を定義する。

  • ① 関数宣言(function 命令)
  • ② 関数式(関数リテラル)
  • ③ Function コンストラクタ(ぶっちゃけ使う必要なし!)
① 関数宣言(function 命令)
function add(x, y) {
    return x + y;
}

この形式で定義された関数は、コード実行時にスコープの先頭まで「関数の巻き上げ」(ホイスティング)が発生する。

② 関数式(関数リテラル)
var add = function(x, y) {
    return x + y;
};

この形式で定義された関数は「関数の巻き上げ」が発生しない。

また、以下のような「名前付き関数式」は、可搬性がないので使わないこと。

var add = function hoge(x, y) {
    return x + y;
};

(参考)【JavaScript】関数定義いろいろ - Qiita


 

高階関数

JavaScript は第一級関数で、高階関数を扱える。

高階関数とは「関数自身を引数や戻り値として扱う関数」(Effective JavaScript では「ほかの関数を引数として受け取るか、結果として関数を返す関数」)。

関数もオブジェクトの一種。

function add(x, y) {
    return x + y;
}

// これが高階関数
function calc(func, x, y) {
  return func(x, y);
}

console.log(calc(add, 1, 2));  // 3

(参考)JavaScriptで関数型プログラミングの入門 - Qiita


 

即時関数

() 演算子を使って、関数を即時実行できる。一度しか実行したくないときに。

(function add(x, y) {
    console.log(x + y);
})(1, 2);  // 3


無名関数を即時実行することで、グローバルオブジェクト(の名前空間)を汚染しないようにすることができる。

(function () {
    var name = 'not global';
})();


また、即時関数を応用すれば、ES5 には存在しない「ブロックスコープ」を擬似的に実現できる。*1

まずダメな例。

var arr = [1, 2, 3],
    results = [],
    i;
for (i = 0; i < arr.length; i++) {
    results[i] = function() { return arr[i]; };
}

results[0]();  // undefined
results[1]();  // undefined
results[2]();  // undefined

results[0](), results[1](), ・・が実行されるタイミングではグローバル変数 i の値が 3 となっていて、結果が undefined になってしまっている。
即時関数を使ってブロックスコープの変数 j を作成して修正したものが以下。

var arr = [1, 2, 3],
    results = [],
    i;
for (i = 0; i < arr.length; i++) {
    (function() {
        var j = i;
        results[i] = function() { return arr[j]; };
    })();
}

results[0]();  // 1
results[1]();  // 2
results[2]();  // 3


 

引数

JavaScript の関数は、引数のチェックを行わない。

それどころか、JavaScript の関数はシグネチャ(引数の型と数)を持たないため、「オーバーロード」という概念も存在しない。


arguments は呼び出し元から渡された引数を管理しているオブジェクトで、暗黙的に生成される。

var checkArgs = function() {
    console.log(arguments[0], arguments[1]);
};

checkArgs(1, 2);  // 1 2
checkArgs(1);  // 1 undefined
checkArgs();  // undefined undefined


 

new 演算子

new は、「オブジェクトのインスタンスを返せ!」とコンストラクタに命令するための演算子。

という説明がすごく分かりやすい。

通常、JavaScriptの関数は return文が明示的に指定されていない場合には呼び出し元に undefined が返される。new 演算子を用いて呼び出された場合は、例外的に、return文の有無に関わらずオブジェクトのインスタンスが返却される。

// コンストラクタ関数
var Person = function(name) {
    this.name = name;
}

// prototype プロパティを使って、オブジェクトにメソッドを追加
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

// new 演算子によって、person.__proto__ に Person.prototype が代入される
var person = new Person('akiyoko');


 

prototype

ES6 で「クラス」の概念が導入されたが、それ以前には JavaScript には「クラス」は存在しない。prototype がクラスの代わりとして使われる。

(いわゆる「クラス」が実現するような)オブジェクトにメンバ(主にメソッド)を追加する仕組みとして、JavaScript では「prototype プロパティ」を使う。

インスタンス化されたオブジェクトは、使用したコンストラクタの prototype プロパティに対して暗黙の参照を持ち、prototype プロパティに追加されたメソッドに対しても同様に暗黙の参照を持つ。

その際、インスタンス化したオブジェクトは、コンストラクタで定義されたメンバ分のメモリを都度確保するが、prototypeプロパティに格納したメソッド分のメモリは確保しないため、メモリ使用量の節約を目的として prototype プロパティにメソッドを追加する。

つまり、

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

とするよりも、

var Person = function(name) {
    this.name = name;
}
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

とした方がメモリの節約になるので、通常は後者のように書く。

メソッドは必ず prototype プロパティで管理するように徹底する

 

プロトタイプチェーン

プロトタイプチェーンは、JavaScript で「オブジェクト指向の継承」を実現するための仕組み。継承したいオブジェクトのインスタンスを、自身の prototype プロパティとして格納する。(なお継承は、ES5準拠の Object.create でも実現可能。)

var Person = function(name) {
    this.name = name;
}
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

var Student = function(name) {
    this.name = name;
}
Student.prototype = new Person();
Student.prototype.study = function() {
    console.log('I am studying now!');
}

ここで、

var akiyoko = new Student('akiyoko');
akiyoko.hello();  // I am akiyoko

を実行する際に、

1)Student オブジェクトの hello メソッドを検索するも、存在せず
   ↓
2)Student オブジェクトが暗黙の参照を持つ Student.prototype(すなわち Person オブジェクト)の hello メソッドを検索するも、存在せず
   ↓
3)Person オブジェクトが暗黙の参照を持つ Person.prototype の hello メソッドを検索して見つかったので、実行!


というプロトタイプチェーンを辿る。


 

this

this が結合する値(レシーバ)は、メソッドおよび関数の呼び出し時に決まる(呼び出され方によって決まる)。

① メソッド呼び出しされた場合

メソッド呼び出しされた場合のメソッド内の this は、メソッドプロパティがルックアップされるオブジェクトに結合される。多くの場合、this は、呼び出し元のオブジェクトを指すことになる。

var akiyoko = {
    name : 'akiyoko',
    hello : function() {
        console.log('I am ' + this.name);  // ★
    }
}

// メソッド内の this(★)は、メソッドプロパティがルックアップされる akiyoko オブジェクトに結合される
akiyoko.hello();  // I am akiyoko

上の例では、hello プロパティがルックアップされた akiyoko オブジェクトを this のレシーバとして、hello が呼び出される。

 

② new 演算子を使ってインスタンス化された場合

new 演算子を使ってインスタンス化された場合のコンストラクタ内の this は、そのインスタンス自身に結合される。

var Person = function(name) {
    this.name = name;  // ★
    this.hello = function() {  // ★
        console.log('I am ' + this.name);
    }
}

// コンストラクタ内の this(★)は、返却されるインスタンス自身に結合される
var akiyoko = new Person('akiyoko');

 

③ 関数呼び出しされた場合

関数呼び出しされた場合の関数内の this は、グローバルオブジェクトに結合される。

var name = 'global';

var hello = function() {
    console.log('I am ' + this.name);  // この this はグローバルオブジェクトに結合される
}

hello();  // I am global

 

④ メソッドおよび関数が高階関数の引数として渡された場合

メソッドおよび関数が Array.prototype.forEach() のような高階関数の引数として渡された場合は、少しやっかい。


以下の例では、akiyoko.hello のレシーバは akiyoko にならない。akiyoko.hello がいつどういった形で呼び出されるのかはその外側のメソッドおよび関数(ここでは forEach *2)の実装次第である。

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

var akiyoko = new Person('akiyoko');
[1, 2].forEach(akiyoko.hello);  // I am I am

レシーバを akiyoko にするためには、bind を使って this を束縛すればよい。 *3

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

var akiyoko = new Person('akiyoko');
[1, 2].forEach(akiyoko.hello.bind(akiyoko));  // I am akiyoko I am akiyoko

別の解決策として、this というキーワードを使わなければよいということで、self, _this, that といった名前の変数に this の参照を「逃がしてやる」方法もある。


次は、イベントリスナーの例。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>JavaScript Test | akiyoko blog</title>
</head>
<body>
<button type="submit" id="submit" name="action" value="send">送信</button>
<script>
    var el = document.querySelector('#submit');
    el.addEventListener('click', function () {
        console.log('this=', this);  // ★
    }, false);
</script>
</body>
</html>

上の例では、addEventListener の引数で渡される無名関数内の this(★)は発火元の DOM要素を指すため、ボタン押下時の実行結果は以下のようになる。

this= <button type=<200b>"submit" id=<200b>"submit" name=<200b>"action" value=<200b>"send"><200b>送信<200b></button><200b>

以下の参考サイトがさらに詳しい。

ハンドラ内でのthisの値について


イベントハンドラの関数内から、その発火元の要素を参照したくなる事がよくあります。 addEventListener()を使用して、関数を割り当てたのであれば、 呼び出し元への参照がthisの値となって関数内に渡されます。


(中略)比較として、仮にHTML内にハンドラが次のように配置されていた場合、

<table id="t" onclick="modifyText();">

onclickイベントで呼び出された際の、modifyText()内のthisの値は、 グローバル(window)オブジェクトへの参照になります。


.addEventListener() | JavaScript 日本語リファレンス | js STUDIO


 

クロージャ

クロージャとは、分かりやすく言うと、

「ローカル変数を参照している、関数の中に定義された関数」

である。

下の例では、

ローカル変数(count)を参照している、関数(outerFunc)の中に定義された関数(innerFunc)

がクロージャ。

function outerFunc(initCount) {
    var count = initCount;

    var innerFunc = function() {
        return ++count;
    };
    return innerFunc;
}

var myCounter = outerFunc(100);
console.log(myCounter());  // 101
console.log(myCounter());  // 102
console.log(myCounter());  // 103

「innerFunc の Callオブジェクト ⇒ counter の Callオブジェクト ⇒ グローバルオブジェクト」というスコープチェーンが生成され、 innerFunc が有効である限りそのスコープチェーンは保持される。innerFunc関数は、myCounter変数に格納されるため、outerFunc関数が呼び出し終わっても破棄されず、従って当該スコープチェーンも破棄されない。これにより、Callオブジェクトに管理されているローカル変数(count)も破棄されず、代入された値が残ったままとなる。


(参考)JavaScript のスコープチェーンとクロージャを理解する - tacamy.blog

*1:ES2015 では let, const を使ってブロックスコープを実現可能。http://qiita.com/tuno-tky/items/74ca595a9232bcbcd727

*2:ちなみに forEach の場合は this はグローバルオブジェクトを指す。

*3:call メソッドや apply メソッドでも this を束縛することができるが、レシーバオブジェクトのほかに引数も指定する必要があるため、このケースでは使用されない。

【ポケモンGO】富士山頂でポケモン獲ったどーー! 頂上でゲットしたポケモンは一体何だった??

Pokemon GO

こんにちは。akiyoko です。
趣味は、山登り(昨年から追加)です。

今年から「山の日」も創設されて、富士山への登山客もますます増えそうですよね。

そんなわけで、私も有給を一日取って、先週末の日曜から月曜日の一泊二日で、富士山での ポケモン狩り 登山 を楽しんできました。ちなみに、昨年に続けて人生二回目の富士山頂への挑戦でした。


登山への出発前、ふとこんな疑問が。

「富士山頂にポケモンっているの??」 と。


せっかくなので、5合目から山頂までの道中でポケモンを集めながら、最終的に富士山頂でポケモンをゲットしてみようと思い立ったのです。

5合目から富士山頂3776m地点(剣ヶ峰)までは二日間合計でおよそ10時間の道中で、アプリをずっと起動しっぱなしだと電池が持たないので、要所要所でアプリを起動してはポケモンを探すというのを繰り返しました。


富士山での歩きスマホは危険だぞ!
ポケモンを探しに富士山に向かう皆様へ|お知らせ|富士登山オフィシャルサイト



それでは、ポケモンゲットの記録を振り返ってみます!!



5合目

一日目の朝9時過ぎ。新宿からの直通バスで5合目に到着。

アプリを起動してすぐにポケストップやジムが見えたのでひと安心。


ここでは早速、ニドラン♂、スリープ、パラス をゲット。
売店の裏手にある小御嶽(こみたけ)神社付近にたくさん集まっているようです。

f:id:akiyoko:20160809231527p:plain:w300


その他にも、ニャース、ビードル、ポッポ、メノクラゲ がいるようですね。

f:id:akiyoko:20160809093027p:plain:w300


少し体を慣らして、11時頃に5合目を出発。


 

6合目

5合目から50分ほど歩くと6合目に到着。
途中、滑りやすい岩道があるので若干注意が必要です。

そして道中の初ポケモンは、6合目地点の イーブイ でした。

f:id:akiyoko:20160809084703p:plain:w300



 

7合目

6合目から7合目までは苦手な砂利道が長く続きます。
通常60分ほどですが、体を慣らすために90分ほどかけてゆっくり歩きました。


7合目では残念ながらポケモンに遭遇できず。
全国から集まったトレーナーたちに狩り尽くされているのでしょうか。。(涙)


 

山小屋(7合目と8合目の中間地点)

7合目を過ぎると急な岩場が現れ、まさに「登る」感じになります。

一日目の宿は、7合目と8合目の間の「東洋館」。
ここではゆっくりとポケモンと戯れることができました。


まずは、ビードル
雲より高く飛び上がってます。

f:id:akiyoko:20160809094156p:plain:w300


翌日の朝。ポッポと御来光。

f:id:akiyoko:20160809094222p:plain:w300



二日目は4時頃に起床。
4:50頃に山小屋の前で御来光を見て、朝食を食べてから6時頃に出発。


 

8合目

ここから岩場がいっそう険しくなります。
30分後に8合目に到着。

8合目の太子館では、イーブイがお出迎え。かわいい。

f:id:akiyoko:20160809100057p:plain:w300


ふと道中のポケモンジムをチェックしてみたのですが、驚きのジムレベル6!! 下界のジムよりもよっぽど鍛えられているじゃないですか!

f:id:akiyoko:20160809100241p:plain:w300



 

本8合目

8合目から本8合目までは結構長く、90分ほどの道のり。

本8合目の富士山ホテル前では、クラブと遭遇 しました。
あー、カニ寿司食べたい。。

f:id:akiyoko:20160809100418p:plain:w300



 

9合目

本8合目から9合目までは約1時間ほど。

このあたりから急に、近付きつつある台風の影響でガスと風が強くなってきました。


小さな社の前で、スリーパーを発見
でも逃げられてしまいました。。

f:id:akiyoko:20160809094357p:plain:w300


 

頂上(久須志神社付近)

この鳥居と狛犬が見えると、頂上はもう目前。
ズバットを一匹 を捕まえて、さっさと先へ進みます。

f:id:akiyoko:20160809094427p:plain:w300


登山道を登り切ると、久須志(くすし)神社があります。
山小屋を出発して4時間での到着です。

ここが一般に、富士山の「頂上」と言われている地点です(が、最高地点ではありません)。


頂上地点には ポケモンがウヨウヨ いました。


まず、ゴース。

f:id:akiyoko:20160809094714p:plain:w300


ウツドン。初めて遭遇しました。

f:id:akiyoko:20160809094740p:plain:w300


クサイハナ。寝てるのかな?

f:id:akiyoko:20160809094815p:plain:w300


ケーシィが店の前でくつろいでいました。

f:id:akiyoko:20160809094843p:plain:w300


ゴースには逃げられてしまいましたが、ウツドン、クサイハナ、ケーシィ をゲットできました!


3776m 地点(剣ヶ峰)

実はここからが本番。
目指すのは、久須志神社から火口の周りをぐるっと一周する「お鉢巡り」の道中にある、剣ヶ峰の頂上が最高地点(3776m 地点)です。

剣ヶ峰到着までに、郵便局でポストカードを出したり、浅間(せんげん)神社奥宮で御朱印を記帳してもらったりしたので、頂上到着から1時間半ほどかかっています。


で、最高地点で待っていたのは「ゴース」でした!

f:id:akiyoko:20160809100537p:plain:w300


続いて、ビードルも出現!

f:id:akiyoko:20160809100609p:plain:w300


最後は、パラス!!

f:id:akiyoko:20160809100637p:plain:w300


他にもピッピが付近にいたようですが、今回は現れてくれませんでした。

f:id:akiyoko:20160809100737p:plain:w300


というわけで、3776m地点(剣ヶ峰)でゲットしたポケモンは、ゴース、ビードル、パラス でした。



 

行程と装備

ここで、今回の富士山頂登山の行程と装備を紹介します。

富士山頂への登山ルートは、吉田口、富士宮口、須走口、御殿場口の4つがありますが、今回(前回も)は初心者向けと言われている「吉田口」を利用しました。初心者向けと言っても、かなり大変でしたが。。

(参考)富士山の4大登山ルートを紹介|初心者のための富士山登山


一日目は、高山病にならないように体を慣らす、ということをメインに過ごしました。7合目と8合目の中間にある山小屋でゆっくり一晩休んだ後、二日目は朝から夕方まで9時間半も歩き続けています。


ちなみに昨年は、初めてということもあって二泊三日でチャレンジしたので、もう少しゆったりしたスケジュールでした。

f:id:akiyoko:20160810021452p:plain



昨年は山グッズを全部レンタルしたのですが、今回は全部新調しました。

ザック(リュック)

一泊二日なら、容量は 35 L くらいがちょうどいいでしょう。これより大きいとそれだけ重くなるので大変です。
ザックカバーが付いているものを選ぶと吉。

 

レインウェア

GORE-TEX のレインウェアだと数万円しますが、天候次第では使わないこともあるので、上下一万円ほどの初心者用のものを購入しました。今回(雨は降らなかったものの)ガスと暴風で一時期ビショ濡れになりましたが、これで十分でした。

防寒着

ユニクロのウルトラライトダウンを防寒着として。
山小屋で寝るときや頂上付近の低温対策に頻繁に使います。

サポートタイツ

膝の負担を減らすために。(ポケモンじゃない)ジムでも使えるタイプ。

(ミズノ)Mizuno バイオギアタイツ(ロング) A60BP300 92 ブラック×ブルー L

(ミズノ)Mizuno バイオギアタイツ(ロング) A60BP300 92 ブラック×ブルー L

 

トレッキングパンツ

サポートタイツの上に履くパンツ。
膝のところでセパレートできて臨機応変に使えるので重宝しています。


帽子

日差しが出ている場合は必須。

靴下

厚手のものと五本指ソックスの二枚履きで。

登山靴

昨年は安物のレンタル靴でつま先が痛くて大変だったのですが、この靴は非常に快適でした。しかも安い!

 

トレッキングポール(ストック)

砂利道が苦手なので、6合目付近からストックを使っています。
軽くて錆びないカーボン製かアルミ製のものがよいかと。

 

サングラス

日差しが強い日は必須。

ヘッドライト

日の出前など、夜間に登山する場合は必須。というか、無いと死にます。

酸素缶

高山病対策として、酸素缶は必携です。
途中の山小屋でも売っていますが、値段は地上の倍くらいします。
このタイプは小さくて携帯にも便利です。容量は10リットルもあれば、ひどい高山病にならないかぎり十分かと。

ユニコム unicom 携帯酸素 ポケットオキシ クリア

ユニコム unicom 携帯酸素 ポケットオキシ クリア

 

充電器

Pokemon GO するなら充電器は必須!
山小屋にコンセントがあるかどうか、事前にチェックしておきましょう。

 

飲み物

「1〜2リットルくらい必要」というのが一般的ですが、私の場合は 500 ml のペットボトル3本あれば一泊二日でちょうどという感じです。
今回はそのうちの一本を「ヨーグリーナ&南アルプスの天然水」にして凍らせて持って行ったのですが、これが大正解!! 冷えた飲み物が疲れた体をリセットしてくれるので、天気が良い日にはオススメです!

サントリー ヨーグリーナ&南アルプスの天然水(冷凍兼用) 540ml×24本

サントリー ヨーグリーナ&南アルプスの天然水(冷凍兼用) 540ml×24本


その他、行動食(カロリーメイトや飴)、日焼け止め、ゴミ袋、耳栓なども必要になるので、しっかり用意しておきましょう。




 

まとめ

  • 3776m地点(剣ヶ峰)でゲットしたポケモンは、ゴース、ビードル、パラス
  • 頂上(久須志神社付近)でゲットしたポケモンは、ウツドン、クサイハナ、ケーシィ
  • 富士山ならではのレアなポケモンはいなさそう??


f:id:akiyoko:20160809100846p:plain:w300

f:id:akiyoko:20160809100915p:plain:w300

f:id:akiyoko:20160809100943p:plain:w300

Django ORM の SQL を出力する方法まとめ

Django 開発環境 データベース Python

Django ORM を使っていると、どういった SQL が発行されているか、クエリの内容を出力したいときが多々あります。

SQL を出力する方法についてはいくつか方法がありますが、今回はその方法を思いつく限りピックアップしてみようと思います。

 

1)QuerySet の query を print する

個人的に一番よく使う方法。
実際に SQL を発行せずに、発行予定の SELECT文を出力することができます。

Django shell だろうが、pdb デバッグ中だろうが、PyCharm のデバッグ中だろうが、いつでも利用することができます。

例えば、Django shell で使う場合はこんな感じ。

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> print User.objects.filter(pk=1).query
SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1


事前に何も設定しなくても、デバッグ中に手っ取り早く発行された SQL が確認できるのが特徴です。しかしながら、発行された SQL をリアルタイムに確認することはできません。


(参考)Show the sql Django is running? - Stack Overflow


 

2)DefaultConnectionProxy の queries を出力する

直前に発行された SQL を確認することができます。
1)とセットで使うことが多いでしょうか。

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> User.objects.filter(pk=1)
[<User: user-1>]
>>> User.objects.filter(pk=2)
[<User: admin>]

>>> from django.db import connection
>>> connection.queries
[{u'time': u'0.000', u'sql': u'SET SQL_AUTO_IS_NULL = 0'}, {u'time': u'0.000', u'sql': u'SET SQL_AUTO_IS_NULL = 0'}, {u'time': u'0.000', u'sql': u'SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21'}, {u'time': u'0.001', u'sql': u'SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 2 LIMIT 21'}]
>>> connection.queries[-1]
{u'time': u'0.001', u'sql': u'SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 2 LIMIT 21'}

発行したクエリは、'time'(実行時間)と 'sql' の dict として、発行された順に後ろに追加されていきます。慣れるまで、出力内容が少し読みにくいのが難点でしょうか。

またこちらの方法でも、発行された SQL をリアルタイムに確認することはできません。


(参考)Show the sql Django is running? - Stack Overflow


 

3)django-debug-toolbar の SQL Panel を使う

プラグインのインストールが必要な物の、一番楽チンな方法。

Django を runserver で起動して、実際に画面を操作してから、右側に表示される SQL Panel を開いて確認するだけです。


django-debug-toolbar の SQL Panel を使うには、条件がいくつかあります。

  • DEBUG = True
  • 'django.contrib.staticfiles' が INSTALLED_APPS に設定済み *1

他にも、settings.py に以下の設定が必要です。 *2

INSTALLED_APPS += ('debug_toolbar',)

def always_show_toolbar(request):
    return True

DEBUG_TOOLBAR_CONFIG = {
    'SHOW_TOOLBAR_CALLBACK': '%s.always_show_toolbar' % __name__,
}


このように、画面からサクサクっと操作が可能です。楽チンですね。

f:id:akiyoko:20160804221039p:plain

f:id:akiyoko:20160804221058p:plain


インストール方法

$ pip install django-debug-toolbar

ちなみに、検証時の環境は以下の通りでした。

  • Python 2.7.6
  • Django (1.9.8)
  • django-debug-toolbar (1.5)


django-debug-toolbar のさらに詳しい説明については、以下の書籍が非常に有用です。「14-04 Django Debug Toolbar」の章に詳しい説明が載っています。
Django 以外にも、Python での開発手法についてのノウハウがいろいろ詰まっていてオススメです。

Pythonプロフェッショナルプログラミング第2版

Pythonプロフェッショナルプログラミング第2版



(参考)


 

4)django-debug-toolbar の debugsqlshell を使う

3)の django-debug-toolbar をインストールしているのであれば、通常の Django shell ではなく debugsqlshell を起動することで、発行される SQL を随時確認することができます。

$ python manage.py debugsqlshell
>>> from django.contrib.auth.models import User
>>> User.objects.filter()

SET SQL_AUTO_IS_NULL = 0 [0.80ms]
SELECT `auth_user`.`id`,
       `auth_user`.`password`,
       `auth_user`.`last_login`,
       `auth_user`.`is_superuser`,
       `auth_user`.`username`,
       `auth_user`.`first_name`,
       `auth_user`.`last_name`,
       `auth_user`.`email`,
       `auth_user`.`is_staff`,
       `auth_user`.`is_active`,
       `auth_user`.`date_joined`
FROM `auth_user` LIMIT 21 [0.19ms]
[<User: admin>]


 

5)django-extensions の shell_plus を --print-sql オプションで起動する

「django-extensions」は、「Django フレームワークの機能を便利に拡張する、管理コマンドやデータベースフィールドなどの詰め合わせ」 *3 です。

django-extensions の shell_plus を --print-sql オプションで起動すれば、4)と同じような機能を使うことができます。

$ python manage.py shell_plus --print-sql
>>> from django.contrib.auth.models import User
>>> User.objects.filter()

SET SQL_AUTO_IS_NULL = 0

Execution time: 0.000056s [Database: default]

SELECT `auth_user`.`id`,
       `auth_user`.`password`,
       `auth_user`.`last_login`,
       `auth_user`.`is_superuser`,
       `auth_user`.`username`,
       `auth_user`.`first_name`,
       `auth_user`.`last_name`,
       `auth_user`.`email`,
       `auth_user`.`is_staff`,
       `auth_user`.`is_active`,
       `auth_user`.`date_joined`
FROM `auth_user` LIMIT 21

Execution time: 0.000391s [Database: default]

[<User: admin>]

この shell_plus には autoloading という機能があるらしいのですが、正直なところ使ったことはありません。



インストール方法

$ pip install django-extensions

settings.py に「django_extensions」を追加。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_extensions',
]

(参考)


 

6)django.db.backends のログレベルを動的に変更

事前の準備がほとんど必要なく、1)や 2)のように明示的に SQL を出力する必要がないので、お手軽に使えます。

Django shell で使うケースが多いかもしれません。

$ python manage.py shell
>>>import logging
>>>l = logging.getLogger('django.db.backends')
>>>l.setLevel(logging.DEBUG)
>>>l.addHandler(logging.StreamHandler())

>>> from django.contrib.auth.models import User
>>> User.objects.filter(pk=1)
(0.000) SET SQL_AUTO_IS_NULL = 0; args=None
(0.000) SET SQL_AUTO_IS_NULL = 0; args=None
(0.000) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 21; args=(1,)
[<User: user-1>]


 

7)settings.py の LOGGING を設定

ログに出力したり、コンソールに出力したりと、いろいろ柔軟に設定可能です。リアルタイムに発行される SQL が確認できるのも、この方法の特徴です。

なお、「DEBUG = True」でないと使えないので注意が必要です。


settings.py の設定例

DEBUG = True
LOGGING = {
    'disable_existing_loggers': False,
    'version': 1,
    'handlers': {
        'console': {
            # logging handler that outputs log messages to terminal
            'class': 'logging.StreamHandler',
            'level': 'DEBUG', # message level to be written to console
        },
    },
    'loggers': {
        '': {
            # this sets root level logger to log debug and higher level
            # logs to console. All other loggers inherit settings from
            # root level logger.
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False, # this tells logger to send logging message
                                # to its parent (will send if set to True)
        },
        'django.db': {
            # django also has database level logging
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False,
        },
    },
}


ただしこの方法だと、ログやコンソールが大量の SQL ですぐに埋め尽くされてしまうので、1)や 2)の方法を使って、確認したい SQL だけをピンポイントに出力するようにした方が開発中は捗るかもしれません。


(参考)django - log all sql queries - Stack Overflow


 

まとめ

今回は、Django ORM の SQL を出力する方法として7種類のやり方を紹介しました。
この中から、目的や状況に応じてやり方を使い分けるようにすると、開発の効率もグンとアップすると思います。

良い Django ライフを!

*1:Django 1.9 では、django-debug-toolbar はデフォルトで INSTALLED_APPS に設定済みです。

*2:ミニマムな設定です。

*3:Pythonプロフェッショナルプログラミング第2版 より引用

Django ORM の select_related, prefetch_related の挙動を詳しく調べてみた

Django データベース

Django ORM の QuerySet には、select_related および prefetch_related というメソッドがありますが、イマイチ使い勝手がよく分からなかったりします。


公式ドキュメントにはこう書いてありますが、

select_related works by creating an SQL join and including the fields of the related object in the SELECT statement. For this reason, select_related gets the related objects in the same database query. However, to avoid the much larger result set that would result from joining across a ‘many’ relationship, select_related is limited to single-valued relationships - foreign key and one-to-one.

prefetch_related, on the other hand, does a separate lookup for each relationship, and does the ‘joining’ in Python. This allows it to prefetch many-to-many and many-to-one objects, which cannot be done using select_related, in addition to the foreign key and one-to-one relationships that are supported by select_related.


select_related は、JOIN 句を作って、SELECT 句に関連するオブジェクトのフィールドを含めることによって動作する。これにより、select_related は 1本のデータベースクエリで関連オブジェクトを取得することができる。しかしながら、多くのリレーション先を辿って JOIN することで大規模な結果セットを取得してしまうことを避けるために、select_related は、foreign key や one-to-one といった「対一」リレーションのみを JOIN する。

一方、prefetch_related は、それぞれのリレーションに対して別々の参照を行い、Python コードで JOIN(相当の処理)を行う。これによって、select_related によって取得できる foreign key や one-to-one リレーションのオブジェクトだけではなく、select_related では取得不可能な many-to-many や many-to-one リレーションのオブジェクトの先取り(prefetch)をすることができる。


(akiyoko 訳)

https://docs.djangoproject.com/ja/1.9/ref/models/querysets/#select-related
https://docs.djangoproject.com/ja/1.9/ref/models/querysets/#prefetch-related


よく分からないですよね。


そこで今回は、select_related, prefetch_related を使うことでどのようなメリットがあるのかを確認するためにその挙動を詳しく調べてみたので、その結果をまとめてみたいと思います。



少し長いので、結論(分かったこと)から先に書きます。


分かったこと

Django ORM について

  • 1)多対一、一対一のリレーション先のオブジェクトはフィールドにアクセスした時点でクエリが発行される
    • クエリ発行後は取得したオブジェクトがキャッシュされるため、それ以降のクエリは発行されない
    • リレーション先のレコードが存在しなくても NotFound は発生しない
  • 2)一対多、多対多のリレーション先のオブジェクト群は、(all や filter で)アクセスする度にクエリが発行される
    • リレーション先のレコードが存在しなくても NotFound は発生しない

select_related について

  • 3)select_related を使うことで、一度のクエリで多対一、一対一のリレーション先のオブジェクトを取得してキャッシュしてくれる
    • null=False(デフォルト) の ForeignKey の場合は INNER JOIN で結合
    • null=True の ForeignKey の場合は LEFT OUTER JOIN で結合
  • 4)select_related の引数を指定しない場合は、多対一、一対一になっているリレーション先を全て取得してくれるのではなく、null=False の ForeignKey のみが取得対象

prefetch_related について

  • 5)prefetch_related を使うことで、先行してクエリを発行して、一対一、多対一、一対多、多対多のリレーション先のオブジェクト(群)を取得してキャッシュしてくれる
    • ただし、クエリの総数は減らない

注意点

  • select_related, prefetch_related の引数は明示的に指定しよう!



 

検証内容

今回は、一対一のリレーションの検証はしていません。 *1

検証環境

  • Ubuntu 14.04.4 LTS
  • Python 2.7.6
  • Django 1.9.8
  • MySQL 5.5.50

サンプルテーブル

ブログの投稿を想定したいくつかのテーブル群を検証に使用します。
ブログ投稿(blogpost)とブログ画像(blogimage)が多対一、ブログ投稿と投稿者(auth_user)が多対一、ブログ投稿とブログカテゴリが多対多のリレーションを持っていると仮定します。

以下は、論理モデルを ER図にしたものです。

f:id:akiyoko:20160803233339p:plain


参考までに、実際に作成したテーブルから、物理モデルの ER図を自動出力したものが以下になります。 *2

f:id:akiyoko:20160803071330p:plain


事前準備

Mezzanine プロジェクトの開発環境を PyCharm で設定する - akiyoko blog
を参考に、Ubuntu に Django アプリを作成します。

$ sudo apt-get update
$ sudo apt-get -y install python-dev git tree
$ sudo apt-get -y install mysql-server
$ sudo apt-get -y install mysql-client libmysqlclient-dev python-mysqldb
$ sudo mysql_install_db
$ sudo vi /etc/mysql/my.cnf
$ sudo service mysql restart
$ sudo mysql_secure_installation
$ mysql -u root -p
mysql> create database myproject character set utf8;
mysql> create user myprojectuser@localhost identified by "myprojectuserpass";
mysql> grant all privileges on myproject.* to myprojectuser@localhost;
mysql> flush privileges;
mysql> exit

$ wget https://bootstrap.pypa.io/get-pip.py
$ sudo -H python get-pip.py
$ sudo -H pip install virtualenv virtualenvwrapper
$ cat << EOF >> ~/.bash_profile

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi
EOF
$ cat << EOF >> ~/.bashrc

source /usr/local/bin/virtualenvwrapper.sh
export WORKON_HOME=~/.virtualenvs
EOF
$ source ~/.bashrc
$ mkvirtualenv myproject
$ sudo mkdir -p /opt/webapps/myproject
$ sudo chown -R `whoami`. /opt/webapps
$ pip install MySQL-python
$ cd /opt/webapps/myproject/
$ pip install Django
$ django-admin startproject config .
$ python manage.py startapp blog
$ vi config/settings.py
(INSTALLED_APPS に blog を追加、DATABASES の設定を MySQL に変更)

config/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
]
    ・
    ・
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myproject',
        'USER': 'myprojectuser',
        'PASSWORD': 'myprojectuserpass',
        'HOST': '',
        'POST': '',
    }
}
$ vi blog/models.py
from __future__ import unicode_literals

from django.db import models
from django.utils.translation import ugettext_lazy as _


class BlogPost(models.Model):
    """
    A blog post.
    """
    class Meta:
        verbose_name = _("Blog post")
        verbose_name_plural = _("Blog posts")

    categories = models.ManyToManyField("BlogCategory", verbose_name=_("Categories"),
                                        blank=True, related_name="blogposts")
    image = models.ForeignKey("BlogImage", verbose_name=_("Featured Image"),
                              related_name="blogposts", blank=True, null=True)
    user = models.ForeignKey("auth.User", verbose_name=_("Author"), related_name="blogposts")
    title = models.CharField(_("Title"), max_length=255)
    content = models.TextField(_("Content"), blank=True)


class BlogCategory(models.Model):
    """
    A category for grouping blog posts into a series.
    """
    class Meta:
        verbose_name = _("Blog Category")
        verbose_name_plural = _("Blog Categories")

    title = models.CharField(_("Title"), max_length=255)


class BlogImage(models.Model):
    """
    A featured image.
    """
    class Meta:
        verbose_name = _("Blog Image")
        verbose_name_plural = _("Blog Images")

    caption = models.CharField(_("Caption"), max_length=255)
    image_url = models.FileField(verbose_name=_("Image URL"), max_length=255, blank=True, null=True)
$ python manage.py makemigrations
$ python manage.py migrate


 

検証

まずは事前準備として、初期データを投入します。

$ python manage.py shell
>>> import logging
>>> l = logging.getLogger('django.db.backends')
>>> l.setLevel(logging.DEBUG)
>>> l.addHandler(logging.StreamHandler())

>>> from blog.models import BlogPost, BlogCategory, BlogImage
>>> from django.contrib.auth.models import User

>>> User(username='user-1').save()
>>> BlogCategory(title='cat-1').save()
>>> BlogCategory(title='cat-2').save()
>>> BlogImage(caption='image-1', image_url='blog/image1.jpg').save()
>>> user1 = User.objects.get(pk=1)
>>> cat1 = BlogCategory.objects.get(pk=1)
>>> cat2 = BlogCategory.objects.get(pk=2)
>>> image1 = BlogImage.objects.get(pk=1)
>>> BlogPost(image=image1, user=user1, content='post-1').save()
>>> post1 = BlogPost.objects.get(pk=1)
>>> post1.categories = [cat1, cat2]
>>> BlogPost(user=user1, content='post-2').save()
>>> post2 = BlogPost.objects.get(pk=2)


 
ここからが、本番です。

 

1)多対一、一対一のリレーション先のオブジェクトはフィールドにアクセスした時点でクエリが発行される
>>> post1 = BlogPost.objects.filter(pk=1)[0]
(0.002) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
>>> post1.user
(0.000) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1; args=(1,)
<User: user-1>
>>> post1.image
(0.001) SELECT `blog_blogimage`.`id`, `blog_blogimage`.`caption`, `blog_blogimage`.`image_url` FROM `blog_blogimage` WHERE `blog_blogimage`.`id` = 1; args=(1,)
<BlogImage: BlogImage object>
1−1)クエリ発行後は取得したオブジェクトがキャッシュされるため、それ以降のクエリは発行されない
>>> post1 = BlogPost.objects.filter(pk=1)[0]
(0.002) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
>>> post1.user
(0.000) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1; args=(1,)
<User: user-1>
>>> post1.user
<User: user-1>
>>> post1.image
(0.001) SELECT `blog_blogimage`.`id`, `blog_blogimage`.`caption`, `blog_blogimage`.`image_url` FROM `blog_blogimage` WHERE `blog_blogimage`.`id` = 1; args=(1,)
<BlogImage: BlogImage object>
>>> post1.image
<BlogImage: BlogImage object>
1−2)リレーション先のレコードが存在しなくても NotFound は発生しない
>>> post2 = BlogPost.objects.filter(pk=2)[0]
(0.001) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 2 LIMIT 1; args=(2,)
>>> post2.image
>>> post2.image is None
True


 

2)一対多、多対多のリレーション先のオブジェクト群は、all や filter でアクセスする度にクエリが発行される
>>> post1 = BlogPost.objects.filter(pk=1)[0]
>>> post1.categories
<django.db.models.fields.related_descriptors.ManyRelatedManager object at 0x7f2509080a10>
>>> post1.categories.all()
(0.001) SELECT `blog_blogcategory`.`id`, `blog_blogcategory`.`title` FROM `blog_blogcategory` INNER JOIN `blog_blogpost_categories` ON (`blog_blogcategory`.`id` = `blog_blogpost_categories`.`blogcategory_id`) WHERE `blog_blogpost_categories`.`blogpost_id` = 1 LIMIT 21; args=(1,)
[<BlogCategory: BlogCategory object>, <BlogCategory: BlogCategory object>]
>>> post1.categories.all()
(0.000) SELECT `blog_blogcategory`.`id`, `blog_blogcategory`.`title` FROM `blog_blogcategory` INNER JOIN `blog_blogpost_categories` ON (`blog_blogcategory`.`id` = `blog_blogpost_categories`.`blogcategory_id`) WHERE `blog_blogpost_categories`.`blogpost_id` = 1 LIMIT 21; args=(1,)
[<BlogCategory: BlogCategory object>, <BlogCategory: BlogCategory object>]
2−1)リレーション先のレコードが存在しなくても NotFound は発生しない
>>> post2 = BlogPost.objects.filter(pk=2)[0]
(0.001) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 2 LIMIT 1; args=(2,)
>>> post2.categories.all()
(0.001) SELECT `blog_blogcategory`.`id`, `blog_blogcategory`.`title` FROM `blog_blogcategory` INNER JOIN `blog_blogpost_categories` ON (`blog_blogcategory`.`id` = `blog_blogpost_categories`.`blogcategory_id`) WHERE `blog_blogpost_categories`.`blogpost_id` = 2 LIMIT 21; args=(2,)
[]


 

3)select_related を使うことで、一度のクエリで多対一、一対一のリレーション先のオブジェクトを取得してキャッシュしてくれる

なお、

  • null=False(デフォルト) の ForeignKey の場合は INNER JOIN で結合
  • null=True の ForeignKey の場合は LEFT OUTER JOIN で結合

となる。

>>> post1 = BlogPost.objects.filter(pk=1).select_related('user', 'image')[0]
(0.001) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content`, `blog_blogimage`.`id`, `blog_blogimage`.`caption`, `blog_blogimage`.`image_url`, `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `blog_blogpost` LEFT OUTER JOIN `blog_blogimage` ON (`blog_blogpost`.`image_id` = `blog_blogimage`.`id`) INNER JOIN `auth_user` ON (`blog_blogpost`.`user_id` = `auth_user`.`id`) WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
>>> post1.user
<User: user-1>
>>> post1.image
<BlogImage: BlogImage object>


 

4)select_related の引数を指定しない場合は、多対一、一対一になっているリレーション先を全て取得してくれるのではなく、null=False の ForeignKey のみが取得対象

なので、select_related の引数は明示的に指定しましょう。

>>> post1 = BlogPost.objects.filter(pk=1).select_related()[0]
(0.001) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content`, `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `blog_blogpost` INNER JOIN `auth_user` ON (`blog_blogpost`.`user_id` = `auth_user`.`id`) WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
>>> post1.user
<User: user-1>
>>> post1.image
(0.000) SELECT `blog_blogimage`.`id`, `blog_blogimage`.`caption`, `blog_blogimage`.`image_url` FROM `blog_blogimage` WHERE `blog_blogimage`.`id` = 1; args=(1,)
<BlogImage: BlogImage object>


 

5)prefetch_related を使うことで、先行してクエリを発行して、一対一、多対一、一対多、多対多のリレーション先のオブジェクト(群)を取得してキャッシュしてくれる

prefetch_related の場合は、JOIN 句を使うのではなく、クエリを別々に発行して、プログラム的にオブジェクト内に結合してくれます。クエリの総数が減ることはありませんので、クエリの発行数を減らすという目的で prefetch_related を使用することはできません。


(多対一の場合)

>>> post1 = BlogPost.objects.filter(pk=1).prefetch_related('user')[0]
(0.001) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
(0.000) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` IN (1); args=(1,)
>>> post1.user
<User: user-1>
>>> post1.__dict__
{'user_id': 1L, 'title': u'', '_user_cache': <User: user-1>, '_state': <django.db.models.base.ModelState object at 0x7f2509033310>, 'content': u'post-1', 'image_id': 1L, '_prefetched_objects_cache': {}, 'id': 1L}


(一対多の場合)

>>> user1 = User.objects.filter(pk=1).prefetch_related('blogposts')[0]
(0.001) SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 LIMIT 1; args=(1,)
(0.000) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`user_id` IN (1); args=(1,)
>>> user1.blogposts.all()
[<BlogPost: BlogPost object>, <BlogPost: BlogPost object>]


(多対多の場合)

>>> post1 = BlogPost.objects.filter(pk=1).prefetch_related('categories')[0]
(0.000) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)
(0.000) SELECT (`blog_blogpost_categories`.`blogpost_id`) AS `_prefetch_related_val_blogpost_id`, `blog_blogcategory`.`id`, `blog_blogcategory`.`title` FROM `blog_blogcategory` INNER JOIN `blog_blogpost_categories` ON (`blog_blogcategory`.`id` = `blog_blogpost_categories`.`blogcategory_id`) WHERE `blog_blogpost_categories`.`blogpost_id` IN (1); args=(1,)
>>> post1.categories.all()
[<BlogCategory: BlogCategory object>, <BlogCategory: BlogCategory object>]
>>> post1.categories.__dict__
{'source_field': <django.db.models.fields.related.ForeignKey: blogpost>, 'reverse': False, 'source_field_name': 'blogpost', '_constructor_args': ((<BlogPost: BlogPost object>,), {}), 'creation_counter': 32, 'target_field_name': u'blogcategory', '_inherited': False, '_db': None, 'query_field_name': u'blogposts', '_hints': {}, 'prefetch_cache_name': 'categories', 'instance': <BlogPost: BlogPost object>, 'through': <class 'blog.models.BlogPost_categories'>, 'core_filters': {u'blogposts__id': 1L}, 'symmetrical': False, 'model': <class 'blog.models.BlogCategory'>, 'related_val': (1L,), 'target_field': <django.db.models.fields.related.ForeignKey: blogcategory>, 'name': None}


 
なお、filter() ではクエリが再発行されてしまうようです。all() を先取りしてキャッシュしているのかな。。

>>> post1 = BlogPost.objects.filter(pk=1).prefetch_related('categories')[0]
>>> post1.categories.filter()
(0.000) SELECT `blog_blogcategory`.`id`, `blog_blogcategory`.`title` FROM `blog_blogcategory` INNER JOIN `blog_blogpost_categories` ON (`blog_blogcategory`.`id` = `blog_blogpost_categories`.`blogcategory_id`) WHERE `blog_blogpost_categories`.`blogpost_id` = 1 LIMIT 21; args=(1,)
[<BlogCategory: BlogCategory object>, <BlogCategory: BlogCategory object>]


 
あと、prefetch_related の引数を指定しないと、リレーション先を取得してくれないように見えます。どこまでリレーション先を取ってくるか分からないから、明示的に指定しないと取得しないという仕様なのかもしれません。

なので、prefetch_related の引数は明示的に指定しましょう。

>>> post1 = BlogPost.objects.filter(pk=1).prefetch_related()[0]
(0.000) SELECT `blog_blogpost`.`id`, `blog_blogpost`.`image_id`, `blog_blogpost`.`user_id`, `blog_blogpost`.`title`, `blog_blogpost`.`content` FROM `blog_blogpost` WHERE `blog_blogpost`.`id` = 1 LIMIT 1; args=(1,)

 

まとめ

select_related や prefetch_related の使い道としては主に、後続の処理で何度もアクセスされるオブジェクトを先に取得しておきたいときに使うのがよい と思います。特に、一側のオブジェクトを取得する場合でクエリの本数を減らしたいなら select_related を、多側のオブジェクト群を取得したいなら prefetch_related を検討すればよいでしょう。


Django ORM は、クエリ(SQL)をあまり意識せずに使えて便利な半面、何も分からずに使っていると、クエリの本数や実行速度がボトルネックになって、応答速度の遅い処理を作ってしまいがちです。

クエリの実行を含む処理の応答速度が遅い場合は、まずはクエリを確認し、select_related や prefetch_related を使って実行速度を改善することも検討すべきです。

*1:一対一リレーションの挙動は、多対一リレーションの場合とほぼ同じでした。双方から ForeignKey が付与されていると考えればよいのかも。

*2:PyCharm Professional のデータベース機能を使いました。http://akiyoko.hatenablog.jp/entry/2016/03/13/141600 を参照