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

akiyoko blog

akiyoko の IT技術系ブログです

【まとめ】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:PayPal(日本語) - ペイパル|サービス|ウェブペイメントプラス

*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 を束縛することができるが、レシーバオブジェクトのほかに引数も指定する必要があるため、このケースでは使用されない。

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

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 を参照

「一対一」「一対多」「多対多」のリレーションを分かりやすく説明する

データベース

こんにちは、akiyoko です。

今回はデータベース設計の話です。
分かりそうでよく分からない、「一対一」「一対多」「多対多」のリレーションを分かりやすく説明してみます。

f:id:akiyoko:20160801054222p:plain


一対一リレーション

f:id:akiyoko:20160731193333p:plain

分かりやすい定義

  • 双方のレコードが一対一に対応する

あるいは、

  • 双方の主キーが同じ


 

設計例

一対一リレーションは、分割しなくてもよいテーブルが分割されている状態です。既存のテーブル構成を変えずに項目を追加したい場合などを除き、積極的に使用する機会はあまり無いように思います。


 

一対多リレーション

f:id:akiyoko:20160731214014p:plain

分かりやすい定義

  • A のレコードは B の複数のレコードと関連する可能性があるが、B のレコードは A のレコードと最大一件のみ関連する

もう少し詳しく説明すると、

  • A から見れば、A の 1つのレコードが同時に複数の B のレコードと関連している(関連のないレコードもある)
  • B から見れば、B の 1つのレコードが A の 1つのレコードのみと関連している(関連のないレコードもある)

分かりやすい具体例

  • 例1)A:ブログの投稿者、B:ブログの投稿
  • 例2)A:顧客、B:注文
  • 例3)A:注文、B:注文明細
  • 例4)A:部署、B:従業員

 

設計例

A(一側)は、正規化によって B(多側)から分割されたテーブルである場合が多いです。

一対多リレーションでは、一側のテーブルの主キーを参照する外部キーを、多側のテーブルに設けて対応する場合が多いでしょう。


 

多対多リレーション

f:id:akiyoko:20160731193422p:plain

分かりやすい定義

  • A のレコードは B の複数のレコードと関連する可能性があり、B のレコードも A の複数のレコードと関連する可能性がある

もう少し詳しく説明すると、

  • A から見れば、A の 1つのレコードが同時に複数の B のレコードと関連している(関連のないレコードもある)
  • B から見れば、B の 1つのレコードが同時に複数の A のレコードと関連している(関連のないレコードもある)

 

分かりやすい具体例

  • 例1)A:ブログのカテゴリ、B:ブログの投稿 *1
  • 例2)A:ユーザ、B:権限 *2

 

設計例

多対多リレーションでは、双方に外部キーを設け、中間テーブルを利用するケースが多いでしょう。


 

参考

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

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

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

*1:※投稿にはカテゴリが複数設定できるという前提

*2:※ユーザには権限が複数設定できるという前提

「SEO初心者に贈るWebライティング講座 ~キーワードからの記事作成編~」に参加しました

勉強会 SEO

主催

株式会社クリーク・アンド・リバー社


会場

株式会社クリーク・アンド・リバー社
東京都千代田区一番町8番地 住友不動産一番町ビル 麹町制作ルーム5F


感想など

二週間以上経ってしまいましたが、今月中旬に「SEO初心者に贈るWebライティング講座 ~キーワードからの記事作成編~」というイベントに参加してきました。


イベントの内容は、

  • ① SEOを意識したWebライティングに欠かせない、キーワードの選定
  • ② キーワードの検索ボリュームやSEO難易度をチェック
  • ③ 複合語で絞り込むユーザーニーズと記事テーマ
  • ④ 設定キーワードを軸にした文章構成術

ということで、Webライティングにあたっての SEOノウハウについての
ちなみに有料(1,000円)でしたが、かなり盛り沢山の内容で気になっていたことも直接質問できたので、個人的には安いくらいでした。


最近、人の気持ちに訴えかけるライティングのコツを身につけるために、ライティングのレジェンドと言われているダン・ケネディの本を読んでみたのですが、今回の内容は、Google のランキングアルゴリズムに訴えかけるライティングのコツについての勉強会です。

究極のセールスレター シンプルだけど、一生役に立つ!お客様の心をわしづかみにするためのバイブル

究極のセールスレター シンプルだけど、一生役に立つ!お客様の心をわしづかみにするためのバイブル

  • 作者: ダン・ケネディ,神田昌典,齋藤慎子
  • 出版社/メーカー: 東洋経済新報社
  • 発売日: 2007/03/30
  • メディア: 単行本(ソフトカバー)
  • 購入: 22人 クリック: 143回
  • この商品を含むブログ (13件) を見る


SEO テクニックについては、少し前の「タイ全裸事件」で 辻正浩さんの Twitter をフォローしたのがきっかけで最近興味を持っていたのですが、SEO についての最新情報や全体像を確認しておきたいと思い、今回のイベントに参加した次第です。


講師は、現在フリーで Web制作・ライターをされている方で、これまで 10年くらい雑誌系のライターや Web制作をしながら、オンライン Webスクールの主催および、ブログ、SEOなどの講師等も歴任してきたとのことです。




メモ

  • Webライティングには 3つのポイント
    • タイトルと説明文
    • パッと見の印象(ファーストビュー)
    • 読みやすく、分かりやすい文章術
  • 被リンク(外部要因)も重要だが、Google はコンテンツ(内部要因)重視
    • 過去のパンダアップデート、ペンギンアップデートによって、業者が 3〜4年くらい前に壊滅状態に
    • コンテンツ is king.
  • テクニック
    • タイトルにサイトの最重要キーワードを必ず含める!
      • タイトルの頭の方に重要なキーワードを入れておく
    • タイトルは 30文字以内で!
    • タイトルは、単語の羅列ではなくきちんとした文章にする
    • タイトルの重複に注意!
    • 内容はユーザへの訴求力も必要
    • まず結論を
    • ユーザーにとってのメリット、ターゲットを明確に
      • 「何杯食べても太らない」「初心者のための」
    • 具体的な表現を。数字を入れると効果高い
    • meta description は SEO的にはもはや効果がないとされているが、SERPs に説明文として表示されるケースがあるので気をつけるべし
      • 検索語で説明文が変わることも
    • 見出しや本文でも適切にキーワードを使用する
    • 代名詞はなるべく使わず、一般名詞を(くどくならない範囲で)使うようにする
    • 内部リンクも SEO に影響するので、「こちら」にリンクを張らないように気をつける(リンク先の要約などにする)
    • 主要な画像には alt を入れる。キャプション(近くに説明文)もあった方がいい
    • 本文の文字数は多い方がいいが、最終的には質が大事
      • ミラーリングは嫌われる。オリジナル要素が重要
  • キーワード選定に役立つツール
  • 検索した人の意図を考えるべし!

Google Analytics の Kindle 本