akiyoko blog

akiyoko の IT技術系ブログです

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

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

 

設計例

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


 

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

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

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

主催

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


会場

株式会社クリーク・アンド・リバー社
東京都千代田区一番町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 本

Mac の MySQL クライアントに「Sequel Pro」を使っているなら PostgreSQL クライアントは「PSequel」がオススメ

タイトル通りですが、Mac の MySQL クライアントに「Sequel Pro」を使っているなのであれば、PostgreSQL クライアントは「PSequel」がオススメです。


長年、Mac の PostgreSQL クライアントに不満があり、使い勝手の良いアプリを探し求めていたのですが、ついにその答えを見つけたような気がします。

私が個人的に一番使い勝手が良いと思っている MySQL クライアントが「Sequel Pro」なのですが、その Sequel Pro の PostgreSQL 版とも言うべきアプリがこの「PSequel」なのです。


PSequel の作者も、このように言っています。

Well, pgAdmin is great for its feature-richness. However, I found its UI is clumsy and complicated. I know there is a list of PostgreSQL GUI Tools. However, they are either web-based, Java-based* or don't support the features I want. In the good old MySQL world, my favorite client is Sequel Pro, but its support for PostgreSQL doesn't seem to be happening. So, I decided to make one myself.


pgAdmin って機能が抱負で良いんだけどさ、UI がイケてないよね。他にも PostgreSQL の GUI ツールはいろいろあるんだけど、Web ベースだったり、Java ベースだったり、欲しい機能が無かったりでちょっとアレだよね。MySQL だったら、僕のお気に入りのクライアントは Sequel Pro なんだけど、どうやら PostgreSQL のサポートはしなさそうだし、じゃあ自分で作っちゃえって思っちゃったわけ。


(akiyoko 意訳)


 
さて今回は、PSequel の使い方、特に PostgreSQL への接続方法について紹介します。


そもそも実際のシステムでは、PostgreSQL 等のデータベースは通常、サーバの外にはポートは開放しておらず、SSH でリモートサーバに乗り込んでサーバ上のデータベースに接続するというパターンが多いでしょう。本番環境や検証環境では、公開鍵認証でログインすることが一般的でしょうが、開発環境では Vagrant 等で仮想サーバを利用することも多く、その場合はパスワード認証を使うこともあるでしょう。


そこで今回は、リモートサーバ上の PostgreSQL に SSH トンネル経由で接続する方法として、

  • 1)サーバに SSH公開鍵認証でログイン ⇒ サーバ上の PostgreSQL に接続
  • 2)サーバにパスワード認証でログイン ⇒ サーバ上の PostgreSQL に接続

という二つのパターンを紹介します *1


接続する PostgreSQL の設定は、以下の通りとします。

項目 設定内容
データベース名 myproject
データベースユーザ myprojectuser
データベースユーザパスワード myprojectuserpass


 

インストール

インストールするには、Homebrew で

$ brew cask install psequel

とするか、公式ページ から app ファイルをダウンロードしてインストールします。


www.psequel.com


 

PostgreSQL への接続

1)サーバに SSH公開鍵認証ログイン ⇒ サーバ上の PostgreSQL に接続

サーバに SSH公開鍵認証でログインして、SSHトンネル経由で PostgreSQL に接続する方法です。

本番環境(PostgreSQL on EC2)として、各種設定は以下を想定しています。

サーバ IPアドレス 52.100.100.100
ログインユーザ ubuntu
SSH秘密鍵 ~/.ssh/aws_p1.pem


f:id:akiyoko:20160729012558p:plain


2)サーバにパスワード認証ログイン ⇒ サーバ上の PostgreSQL に接続

サーバに SSH公開鍵認証でログインして、SSHトンネル経由で PostgreSQL に接続する方法です。

開発環境(PostgreSQL on Vagrant)として、各種設定は以下を想定しています。

サーバ IPアドレス 192.168.32.10
ログインユーザ vagrant
ログインパスワード vagrant


しかしながら、ここでハマりポイントがあります。
実は、PSequel はパスワード認証による SSHトンネリング機能をサポートしていないため(そして今後もサポートする予定は無さそう *2)、Vagrant のセッティングから SSH秘密鍵のパスを参照することができるという裏ワザ(?)を利用します。

The biggest requirement for a GUI of any sort (whether it is for a database client or something else), is the ability to use SSH tunneling to connect to the server. SSH tunneling allows an application to first connect to a machine (in this case a virtual machine managed by Vagrant), and act as normally as if it were connected directly to a service on your personal machine.


While PSequel does support SSH tunneling, what it does not do is support password authentication. I’m sure there was a good reason for this, but in the case of Vagrant it is irritating nontheless. Most Vagrant boxes have a default username and password (vagrant and vagrant respectively). This makes it incredibly simple to use applications that support SSH tunneling.


In the case of PSequel, the only authentication method that is supported is key authentication. Luckily for us, Vagrant does typically utilize this method of authentication (when you SSH into the Vagrant machine, for example, a private key is used so you don’t have to enter a password). Unfortunately for us, the location of this private key can be different for every machine. If you don’t know how to find it, using PSequel can seem impossible.


To find the private key for a Vagrant box, open up a terminal, navigate to the project folder, and type vagrant ssh-config. You’ll see something like this:


http://zacharyflower.com/using-psequel-with-vagrant/

操作は簡単で、Vagrant のホームに移動して「vagrant ssh-config」コマンドを実行すれば OK です。

$ cd vagrant/ubuntu14/
$ vagrant ssh-config
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2200
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/akiyoko/vagrant/ubuntu14/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL


 
これを利用して、以下の接続設定を行えばよいでしょう。

f:id:akiyoko:20160729012628p:plain

f:id:akiyoko:20160729012651p:plain


ただし正確に言えば、パスワード認証でサーバにログインできない問題が解決できたわけではないので、Vagrant 以外だったらどうするか?については今後の課題でしょうか。




ここからは、MySQL クライアントの「Sequel Pro」*3 の接続設定についてのメモです。

「Sequel Pro」の接続設定

1)サーバに SSH公開鍵認証ログイン ⇒ サーバ上の PostgreSQL に接続

本番環境(PostgreSQL on EC2)として、各種設定は以下を想定。

サーバ IPアドレス 52.100.100.100
ログインユーザ ubuntu
SSH秘密鍵 ~/.ssh/aws_p1.pem

f:id:akiyoko:20160729012257p:plain

2)サーバにパスワード認証ログイン ⇒ サーバ上の PostgreSQL に接続

開発環境(PostgreSQL on Vagrant)として、各種設定は以下を想定。

サーバ IPアドレス 192.168.32.10
ログインユーザ vagrant
ログインパスワード vagrant

f:id:akiyoko:20160729012527p:plain

*1:2015年1月より、PSequel に SSH トンネリング機能がサポートされました。https://twitter.com/psequel/status/550881414540173312

*2:https://github.com/psequel/psequel/issues/17

*3:ちなみに、Sequel は「シクォル」と発音するようです。

「PayPal Tech Meetup #2」に参加してきました

会場

イベント&コミュニティスペース dots.
東京都渋谷区宇田川町20-17 NOF渋谷公園通りビル 8F


Twitter

twitter.com


Togetter

togetter.com


感想など

今年の 3月くらいから個人的に PayPal 決済連携の検証をしていたので、今回の Meetup はすごく勉強になりました。特に、PayPal の Okamura Junichi さんに直接疑問をぶつけることができたのが一番の収穫でした。


今回ゲットした情報ですごく有益だったものをいくつかピックアップ。



あと、ドキュメントがとっ散らかっていた印象を受けた 3月頃に比べて、現在は、PayPal の公式ページが徐々にリニューアルされて情報が整理されつつあるようです。

有益な情報源をいくつかゲット。



前回のイベント「PayPal Tech Meetup #1」の存在を知らなかったのですが、知りたかった機能についていくつか紹介されているっぽいです。検証時に知っていればもっと楽だったのになぁ。。



それはそうと、今回の Meetup は豪華賞品付きのデモ大会でした。テーマは「PayPalまたはBraintree APIを使ったアプリ(Web、モバイル、デバイスなど種類は問いません)」で、10人の参加者が LT形式で 5分ずつの枠でプレゼンを行いました。


私は 5番目に発表した @eiden 永田雅文さんの「Botにコーヒーを頼むアプリ」に投票しましたが、結果は二位でした。惜しいっ!!
一位は @kameturu 亀井浩明さんの「暮らしの中のPayPal」でした。
おめでとうございます!!


f:id:akiyoko:20160615212945j:plain



 

1. PayPalボタンで商品を実際に売ってみたよ

@n0bisuke さん(株式会社LIG)

  • サーバ側から push する機能を作ってる
  • Service Worker + WebPush
  • PayPal Button + Notifications API
  • PayPal の Webhook は Https しか対応してない
  • RequestBin を使う
  • netlify
  • SendPulse


 

2. Instagramでお買い物

新井健三さん(Rascal)


 

3. 暮らしの中のPayPal

@kameturu 亀井浩明さん(Origami)


  • ECじゃなくて、Raspberry Pi を使って通常の店舗でスマホを使って PayPal を使う
  • One-Touch を使いたかった。。
  • PayPal REST API (Payments API)
  • Eddystone で飛ばした(iBeacon は将来対応?)


 

4. 専属家政婦くん

@tokutoku393 ちゃんとくさん

  • 何の成果も得られませんでした!


 

5. Botにコーヒーを頼むアプリ

@eiden 永田雅文さん(株式会社Showcase Gig)



 

6. MessangerAPIで課金

@bboobbaa 吉澤和香奈さん(株式会社CAMON.TOKYO)


  • ベトナム人向けに簡単にチケットを売りたい
  • ベトナム人ほとんどFacebook使ってる
  • Messenger でボットを使う


 

7. PayPal APIとAWSで作る動画コンテンツマーケットサイト

@takeyuweb 竹内雄一さん



 

8. テラレン@LINE窓口

@matsubokkuri 松倉友樹さん


  • テラレン
  • PayPal Subscription を使ってる
  • LINE bot で窓口を自動化


 

9. ちょっと寄付休憩はいりまーす

keigohtr 服部圭悟さん(Apitore)

  • 寄付と昼寝
  • PayPal In-Context Checkout


 

10. ブロックチェーンで資産管理

jkkch 菊池条さん


  • デジタルコンテンツの所有権を管理
  • オンラインカードショップの ECサイト
  • ブロックチェーンは Eris を使用

「Vue.js Tokyo v-meetup="#1"」に参加してきました

会場

株式会社プレイド
東京都品川区西五反田1-5-1 五反田サンケイビル 4F


Twitter

twitter.com


感想など

Vue.js は、シンプルなデータバインディング機能(控えめなリアクティブシステム)を持った MVVM フレームワークで、そのシンプルさをウリにしています。

Vue.js (発音は / v j u ː /、view と同様) はインタラクティブな Web インタフェースを構築するためのライブラリです。Vue.js のゴールは、できる限りシンプルな API でリアクティブデータバインディング と 構成可能な View コンポーネントを提供することです。


概要 - vue.js

その他にも、

  • 拡張可能なデータバインディング
  • 純粋な JavaScript オブジェクトモデル
  • 理に適ったシンプルな API
  • コンポーネントで UI をビルド
  • 小さいライブラリとマッチしてミックスできる

といった特徴があるようです。(「公式ドキュメント」より)


私はフロントエンジニアではなくて、少し前に隆盛を極めた AngularJS の 1系や、最近 Webフロントエンド界隈で注目を集めている React.js には全然付いて行けていないので、その対抗馬としてシンプルで学習コストが低いという噂の Vue.js をひと目見てみようと、今回特に何の事前知識もなく敵情視察に来たわけですが、なかなかのアウェー感で歯がゆい想いをしました(笑)。


そもそも Vue.js は 2013年に誕生し、2015年10月に 1.0 がリリースされ、この 4月に 2.0 がアナウンスされています(現在ベータ)。
そして、人気急上昇中の PHP フレームワーク「Laravel」のコミュニティにフロントエンドライブラリとして採用された、というのも興味深いところです。

Taylor Otwell, the author of Laravel, picked up Vue.js instead of React as he was searching for a new front-end library. Soon afterwards Jeffrey Way created a screen cast series on Laracasts which popularized Vue.js in the Laravel community. Today some of the most active Vue.js users are from the Laravel community and there are really cool open source projects like Koel built with the two technologies combined.


Vue.js: 2015 in Review

(参考)
Learning Vue 1.0: Step By Step


話題の JSフレームワークという括りで Vue.js/React.js/Ember.js/Knockout.js を比較したトレンドを見てみると、React.js となかなかいい勝負をしているように見えます。



しかしながら、AngularJS パイセンをキーワード候補に入れた途端、その圧倒的な人気を前にして他のフレームワークがひれ伏すことになります。。

勉強会のあと、公式ドキュメント を読んだりしたところによると、JavaScript 以外のサーバサイドフレームワークとの相性も良さげで、お手軽にデータバインディングを試せそうだなと感じました。


8分で入門 Vue.js

@hashedrock さん


  • 一番使うバインディング機能の説明
  • いろいろ組み合わせが可能
    • Vue.js+SVG
    • Vue.js+Canvas
    • Vue.js+D3.js
  • なぜ Vue.js なのか?
    • デザイナーと開発者のハイブリッド


 

ViewModelのダイエット Messengerパターン編

@kitak さん

Vue.js Tokyo v-meetup="#1"でLTしてきた、と発表からカットしたMVVMのあれこれ - kitak.blog

  • M・V・VMに分割
  • 揮発性の現象
    • ダイアログや画面遷移など(は本来消失すべき)
    • 元々 ViewModel で扱っていた状態や振る舞いが、ダイアログの状態と混ざってしまう
  • VM が Messenger を使ってイベントを発火する
  • $emit で投げて Vue.directive で受け取る



 

Vue, from view to view

@lepture さん

  • component は UI
  • widget はサーバとやり取り


 

翻訳から始めるVue.js入門

@hypermkt さん

  • vue-validator 日本語ドキュメントが昨日リリース
  • JTF日本語標準スタイルガイド(翻訳用)に準拠
  • textlint
    • テキスト向けのLintツール
  • 一部カスタマイズ
  • 翻訳してもVue.jsには詳しくならない。。


 

Vue.js体験記(2ヶ月目)

@sibukixxx さん

  • サーバサイドは Laravel
  • マニュアルではわからなかったことも
  • 0.1系の古いシンタックスのままの記事が多い
  • webpack の存在を忘れていた
    • vue file を component に分けても require できない
  • requirebin の webpack版「WebpackBin
  • awosome-vue にいろいろサンプル集が載ってた


 

SPA with Vuex + Vue-Router

@tejitak さん


  • SPA やるなら他にもいろいろなフレームワークがある
    • Ember は海外で人気、Riot は最近出てきた
  • 何でも SPA にすればいいものでもない
    • 本当にパーシャルレンダリングが必要?
    • SEO 効かないとか
  • Vuex は flux アーキテクチャ、redux と同じ考え方
    • Store, Mutation, Action
  • 双方向バインディングは禁止?
  • 両方使うときに便利なのが、Vue-router-sync



 

Evan You Q&A セッション with Skype

Q. 今後、2.0 の展望は?

  • 疎結合で並列で開発中。vue core はほぼ出来上がってる。ほかはまだ出来上がってないので、2.0 はまだリリースできない
  • スポンサーが出資してくれる人が増えてきたので、個人で会社を立ち上げるらしい。vue.js の未来は明るいよ

 

Q. Vue.js を選ぶ一番の理由は?

  • simple, faster, smaller
  • 学ぶの簡単なので、トレーニングコストが低いのはビジネス面で有利
  • Redux, React はコミュニティが大きい、バックグラウンドがしっかりしている。Vue.js もこれから追いつくよ
  • 技術的なメリットはパフォーマンス。React は大きいプロジェクトではオプティマイズが大変

 

Q. Webworker を使ってパフォーマンス上げる予定ある?

  • 予定はない。60 msec 内に UI をアップデートできているので、今のところ必要性が無い
  • 将来的に実装は可能ではある

 

Q. 2.0 のコードネームは?

  • Ghost in the shell

 

Q. 少ないエンジニアで開発できるモチベーションは?

  • React は大きい会社がバックにいて、いろんな需要が満たされるようにしないといけない。React はニーズに応える必要があるけど、Vue.js は使ってくれるデベロッパーをハッピーにしたい。アイデアが根本的に違う
  • Facebook はリクルーティング目的で OSS にした?

 

Q. AltJS や 特定のビルドシステムを推奨することはある?

  • 直接使わせるようなことはない。実際に裏で使ってる人はいるはず

ゼロからはじめる Django で ECサイト構築(その3:Django Oscar の機能を調べる)

Django 製の ECパッケージの決定版とも言える Django Oscar は、公式ドキュメント によると、以下の 16 個の機能(Django App)から構成されています。
Oscar Core Apps explained — django-oscar 1.3 documentation



機能
概要
備考
Address 住所登録 配送先住所や請求先住所を管理する
Analytics アナリティクス 商品およびユーザーについてのアナリティクスが利用できる
Basket バスケット ショッピングカート機能が利用できる
Catalogue カタログ 商品カテゴリを管理する
Checkout 決済手続き 特定のフローに沿って決済手続きができる
Customer 顧客管理 在庫切れアラートやメール送信など、顧客とのやり取りを行う。他にも、メンバーシップやマイページのほとんどの機能がここに含まれる
Dashboard ダッシュボード 商品カタログ管理、商品注文管理、在庫・出庫管理、オファー管理などのバックオフィス機能のためのダッシュボード。Django admin サイトの代わりとして利用する
Offers オファー 柔軟な条件のディスカウントを設定することができる
Order 商品注文 在庫数のチェックロジック、ディスカウントや配送料を含めた料金計算ロジックを提供する。画面は無い
Partner パートナー 商品の仕入先パートナー(Supplier)を登録したり、在庫・出庫パートナー(Fulfilment Partner)が利用する情報を提供したりする機能。SKU、在庫数、価格などが記録された在庫元帳(Stockrecord)、税率ポリシーや在庫ポリシーを持つストラテジー(Strategy)を管理することもできる。なお、在庫元帳とストラテジーから、商品を購入するのに必要な情報(PurchaseInfo)を取得する
Payment 購入情報 特定の決済代行サービスのための機能を利用することができる
Promotions 広告 広告のためのコンテンツブロックを作成し、特定の位置(トップページ、検索結果ページなど)に表示することができる
Search 商品検索 商品検索機能が利用できる
Shipping 配送料計算 配送先住所やショッピングカートの合計重量、その他いろいろな条件に応じて、自動的に配送料の計算をすることができる(ように設計されている) *1
Voucher バウチャー いろいろなタイプのクーポンコードを設定することができる
Wishlists ウィッシュリスト ほしい物リストが利用できる





今回は、前回 構築した Django Oscar の Sandbox サイトを実際に動かしながら、Django Oscar の機能の一覧を調べていくことにします。


f:id:akiyoko:20160611012326j:plain





 

メンバーシップ

アカウント登録、退会、パスワード変更、ログイン、ログアウトの機能はひと通り揃っています。


 

アカウント登録

メールアドレスをベースにしたアカウント登録をすることができます。基本的なバリデーションはデフォルトで実装されています。

f:id:akiyoko:20160611113759p:plain

退会

少しわかりにくいですが、退会は、プロフィールページ(右上の[Account]>[Profile]で遷移)から[Delete Profile]を押下することでアカウントを削除することができます。

f:id:akiyoko:20160608011151p:plain
f:id:akiyoko:20160608004823p:plain

パスワード変更

パスワード変更もプロフィールページから操作することができます。

f:id:akiyoko:20160608011011p:plain
f:id:akiyoko:20160608071145p:plain

上記の方法とは別に、ログインページからもパスワードの変更(リセット)が可能です。

f:id:akiyoko:20160608004522p:plain

パスワードリセットのためのメールを送信することができます。
f:id:akiyoko:20160608071816p:plain

You're receiving this e-mail because you requested a password reset for your user account at example.com.


Please go to the following page and choose a new password:

http://example.com/en-gb/password-reset/confirm/Mw/4ck-c1f54b78fdaa9e31e057/

メール本文のリンクをクリックすると、パスワード変更ページに遷移します。

f:id:akiyoko:20160608071558p:plain

ログイン

Sandbox では、ログインとアカウント登録は、同一画面で行うように実装されています。

f:id:akiyoko:20160608011309p:plain

ログインせずに、ゲストとして商品を検索したり商品を注文したり *2 することも可能です。

ログアウト

任意でログアウトも可能です。

f:id:akiyoko:20160608072131p:plain

 

マイページ

右上の[Account]からアカウントごとのページ(いわゆるマイページ)に遷移することができ、そこでは以下の機能を利用することができます。


f:id:akiyoko:20160608013201p:plain

プロフィール(Profile)

プロフィールページでは、氏名やメールアドレス、アカウント登録日時を表示できるほか、パスワード変更や退会を行うことができます。

f:id:akiyoko:20160608213427p:plain

注文履歴(Order History)

商品注文の履歴一覧、および詳細を確認することができます。
また、注文履歴の詳細画面から再注文をすることもできます。

f:id:akiyoko:20160609074548p:plain


登録済み住所一覧(Address Book)

登録した住所は、デフォルトの配送先住所(shipping address)、デフォルトの請求先住所(billing address)として設定しておき、商品注文時に再利用することができます。

f:id:akiyoko:20160611165642p:plain

メール一覧(Email History)

システムから送信されるメールの一覧を確認することができます。

f:id:akiyoko:20160610004545p:plain

在庫切れアラート(Product Alerts)

在庫切れになっている商品の詳細画面から[Notify me]ボタンを押下することで、サイト管理者にアラートを通知することができます。
f:id:akiyoko:20160610030515p:plain

マイページの[Product Alerts]から、通知した在庫切れアラートの一覧とそのステータス(サイト管理者が変更する)を確認したり、アラートを自らキャンセルしたりすることができます。
f:id:akiyoko:20160610030538p:plain

再入荷通知(Notifications)

在庫切れアラートに対するサイト管理者からのレスポンスとして、再入荷通知が行われます。
f:id:akiyoko:20160609074655p:plain

ウィッシュリスト(Wish Lists)

商品検索時にウィッシュリスト(ほしい物リスト)に入れていた商品を確認することができます。

リストは複数作成でき、それぞれのリストに名前を付けることができます。

f:id:akiyoko:20160608214516p:plain
f:id:akiyoko:20160608214545p:plain

 

商品紹介

商品紹介機能には、以下のようなサブ機能があります。

 

商品カテゴリ別表示

Sandbox サイト構築時に、ダミーの商品が 200点ほど自動的に登録されます。それぞれの商品にはカテゴリが付けられていて、商品カテゴリ別の一覧表示をすることができます。

f:id:akiyoko:20160608215231p:plain

商品検索

右上の検索窓から、商品のキーワード検索をすることができます。ちなみに、検索のバックエンドはプラガブルに入れ替え可能で、デフォルトで Haystack の各種検索エンジンを利用することができます。

f:id:akiyoko:20160608204907p:plain


商品の並び替え

商品検索をした後、以下の条件で並べ替えをすることができます。

  • キーワードへの関連性(Relevancy)
  • ユーザー評価の高い順(Customer rating)
  • 価格の高い順(Price high to low)
  • 価格の低い順(Price low to high)
  • タイトルの昇順(Title A to Z)
  • タイトルの降順(Title Z to A)

f:id:akiyoko:20160608204132p:plain



レビュー(ユーザー評価)

商品詳細ページで、レビュー(ユーザー評価)を書き込むことができます。
レビューの公開設定は、サイトの設定で承認制にすることも可能です。

f:id:akiyoko:20160608082137p:plain


レコメンド(おすすめ商品)

商品詳細ページの下の方に、おすすめ商品一覧(Recommended items)が表示されます。なお、Sandbox では、おすすめ商品はダッシュボード上でサイト管理者が手動で設定する仕組みになっています。

f:id:akiyoko:20160610103746p:plain


最近チェックした商品一覧

商品詳細ページの下の方に、最近チェックした商品一覧(Recommended items)が自動的に表示されます。

f:id:akiyoko:20160610103851p:plain



商品注文

商品注文としては、次の機能が含まれています。

 

ショッピングカート(Basket)

Amazon での「カート」、楽天市場での「買い物カゴ」と同等の機能が使えます。

商品詳細ページの[Add to basket]ボタンを押下することで、ショッピングカートに商品を追加することができます。

f:id:akiyoko:20160609073056p:plain

右上の[View basket]ボタンから、カートの中身をチェックすることができます。

f:id:akiyoko:20160609073117p:plain

ショッピングカートはこのように表示されます。

f:id:akiyoko:20160609073312p:plain

ショッピングカートの状態(アイテムの中身)は Cookie に保存され、Sandbox のデフォルト設定では一週間保持されることになります。



商品注文(Order)

ショッピングカートの状態に応じて、適切なディスカウントが自動的に適用されます。また、ユーザーに入力されたクーポンコードによるディスカウントについてもここで合わせて計算されます。

f:id:akiyoko:20160609073312p:plain

なお、通常の商品注文だけではなく、プレオーダー(予約注文)やバックオーダー(在庫切れ商品の取り寄せ注文)も可能です。また設定次第で、ゲスト注文(アカウント登録せずに商品注文すること)を許可することもできます。



決済手続き(Checkout)

Oscar の決済手続きは通常、以下のステップに沿って進められます。

1.ゲスト注文不可ならアカウント登録へ(ログイン済みの場合はスキップ)
   ↓
2.配送先入力
   ↓
3.配送方法選択(配送方法が一つのみの場合はスキップ)
   ↓
4.決済方法選択
   ↓
(PayPal ページにリダイレクト)
   ↓
(PayPal 買い手アカウントで認証して、決済内容を承認)
   ↓
5〜7.プレビュー & 決済内容詳細表示 & 注文確定(支払い)
   ↓
8.「ご注文ありがとうございます」

Checkout — django-oscar 1.3 documentation を参考に改編)


商品の決済手続きをする場合は、ショッピングカートページから「決済手続きに進む(Proceed to checkout)」ボタンを押下します。
f:id:akiyoko:20160609073349p:plain

(ログイン済みなので自動的に)配送先入力ステップに進みます。ここではデフォルトの配送先を選択します。
f:id:akiyoko:20160609073426p:plain

配送方法選択ステップはスキップされ、決済方法選択に進みます。
ちなみに今回は、Sandbox 構築時に決済モジュールとして PayPal しか設定していないため、ここでは PayPal 以外の選択肢がありません。
f:id:akiyoko:20160609073446p:plain

PayPal のログインページにリダイレクトされます。ここで PayPal の買い手アカウントでログインして、
f:id:akiyoko:20160609073647p:plain

決済内容を承認すると、
f:id:akiyoko:20160609073709p:plain

ECサイトのプレビューページに戻ってきます(決済はまだ確定していません)。

プレビューページの「注文確定(Place order)」ボタンを押下すると、支払いが完了します。
f:id:akiyoko:20160609073732p:plain

最後に確認ページが表示されます。
f:id:akiyoko:20160609073807p:plain


 

ダッシュボード(Dashboard)

ダッシュボードでは、商品カタログ管理、商品注文管理、在庫・出庫管理、オファー管理などのバックオフィス機能を利用することができます。

ダッシュボードのホーム画面には、指標となるデータがまとめて一覧表示されています。

f:id:akiyoko:20160610022412p:plain


ダッシュボードで利用できる機能としては、主に以下のようなものがあります。


 

商品カタログ管理(Catalogue)

商品カタログ管理機能では、単に商品を登録するだけでなく、様々な付加情報を設定することができます。

商品カテゴリ登録(Categories)

カテゴリとサブカテゴリを親子関係で紐付けることで、ツリー構造の商品カテゴリを実現することができます。Structure には、親カテゴリ、子カテゴリのほか、スタンドアロンの 3種類のいずれかを指定することができます。

なお、商品カテゴリは、主にナビゲーションのためだけに使用されます。


最上位の商品カテゴリは、[Catalogue]>[Categories]から管理します。

f:id:akiyoko:20160609071117p:plain

[Number of child categories]のリンクか、[Actions]>[Edit children]から辿ってサブカテゴリを編集することができます。


商品登録(Products)

[Catalogue]>[Products]で移動できる商品一覧ページから、商品を登録・編集することが可能です。

ちなみに、商品一覧ページには在庫数が表示されておらず、少し不便です。私個人的にはデフォルトで表示させてほしいところです。

f:id:akiyoko:20160609071138p:plain

商品は、事前に作成した「商品タイプ(Product type)」に紐付ける必要があります。


商品を選択後、[Stock and pricing]タブから仕入先パートナーや在庫数、通貨、価格を設定したり、[Upselling]タブからレコメンド(おすすめ商品)を設定したりすることができます。

f:id:akiyoko:20160611142403p:plain
f:id:akiyoko:20160610030319p:plain


 

在庫・出庫管理(Fulfilment)

在庫・出庫のために必要な情報を提供します。

商品注文管理(Orders)

[Fulfilment]>[Orders]から、商品注文の一覧を確認することができます。

ユーザー側で決済まで完了した商品注文は「Pending」ステータスになっているため、例えば、出庫の受付が済んだら「Being processed」に、配送が完了したら「Complete」に、というふうにステータスを切り替えることができます(手動で切り替えます)。
f:id:akiyoko:20160609081752p:plain

パートナー登録(Partners)

[Fulfilment]>[Partners]から、パートナーを作成したりパートナー一覧を確認したりすることができます。

パートナーには、ユーザーを任意で紐付けることができます。
f:id:akiyoko:20160609081831p:plain

顧客管理(Customers)

ユーザー管理(Customers)

[Customers]>[Customers]から、ユーザーごとの注文履歴や登録済みの住所、商品レビューの一覧などの情報を確認できるほか、パスワードリセットのためのメールを送信するボタンも用意されています。
f:id:akiyoko:20160609082251p:plain


在庫切れアラート管理(Stock alert requests)

[Customers]>[Stock alert requests]から、ユーザーからリクエストされた在庫切れアラートの一覧を確認することができます。
f:id:akiyoko:20160610031030p:plain


オファー管理(Offers)

オファー管理では、ディスカウントとバウチャーの管理をすることができます。

 

ディスカウント登録(Offers)

[Offers]>[Offers]から、ディスカウントを登録・編集できます。
f:id:akiyoko:20160609071005p:plain

Sandbox にデフォルトで登録されている以下の三種類のディスカウントのほか、ボリュームディスカウントなど柔軟な条件のディスカウントを設定することができます。

  • 1)規定個数以上買えば 1つ無料
    • ショッピングカートに 規定個数以上の商品を入れれば、その中で最も価格が安い商品が 1つ無料になるというタイプのディスカウントです。デフォルトの「Normal site offer」は、3個以上で 1つ無料という設定になっています。
  • 2)購入後特典(deferred benefit)
    • 購入後にポイントがもらえる的なディスカウントも設定可能です。Sandbox の「Deferred benefit offer」の説明には「商品を買ったら名前が Barry になるよ」というのが書かれているのですが、イギリスのギャグ(?)なのでしょうか。。(ちなみに、本当に購入後にユーザー名が Barry になってしまいます 笑)
  • 3)配送料無料

f:id:akiyoko:20160609071038p:plain


クーポンコード登録(Vouchers)

[Offers]>[Vouchers]から、クーポンコードを登録・編集することができます。ディスカウントは条件に応じて自動的に適用されるのに対し、クーポンコードは、ユーザーがコードを適宜入力する必要があります。


クーポンコードの登録にあたってはいろいろ柔軟な指定ができるようになっていて、例えば、それぞれのクーポンコードの使用制限について、以下の中から選択することができます。

  • 誰でも一回だけ使用可
  • 誰でも何回でも使用可
  • 先着一名のみ一回だけ使用可

また、割引きの種類については以下の中から選択可能です。

  • パーセント指定で割引
  • 金額指定で割引
  • 配送料のパーセント指定で割引
  • 配送料の金額指定で割引
  • 配送料が固定金額に

f:id:akiyoko:20160609071209p:plain
f:id:akiyoko:20160609071242p:plain


コンテンツ管理(Content)

アバウトページなどの固定ページを編集できるほか、[Content]>[Reviews]からユーザーレビューを確認することもできます。

f:id:akiyoko:20160609082027p:plain



レポート出力(Reports)

レポート出力機能では、様々なレポートを出力することができます。

f:id:akiyoko:20160611151345p:plain


決済状況管理(PayPal)

こちらは決済モジュールに django-paypal を利用した場合に使えるようになる機能ですが、[PayPal]>[Express transactions]から、ユーザーが PayPal 決済のトランザクションで使用した決済API のレスポンスの一覧および詳細を確認することができます。

これにより、決済処理中のエラーをトラッキングして解決に役立てることができるようになります。

f:id:akiyoko:20160609082133p:plain
f:id:akiyoko:20160609082200p:plain



 

まとめ

もちろん今回調査した機能以外にも、様々な機能が Django Oscar にはデフォルトで備わっています。しかしながら、今回の調査結果で分かるように、Django Oscar には、ECサイトを運営する上で必要となる基本機能はだいたい揃っていると考えてよいでしょう。


あとは、日本で Django Oscar を利用して ECサイトを構築したときに、日本の商習慣に合わせたカスタマイズ、ローカライズが必要になってくると考えられます。そこは追々やっていくとして、今後は、Oscar の機能をもう少し深追いしてみたいと思います。

*1:How to configure shipping — django-oscar 1.3 documentation

*2:ゲスト注文を許容するかどうかは設定次第