ANNAIマガジン
drupal8-node-structure-1
この記事の目次

この記事は 「Drupal Advent Calendar 2017」の12月15日分の記事です。

ANNAIの青山です。

Drupalは非常に柔軟で拡張しやすいデータ構造を持っている事で知られています。例えば、コンテンツタイプというデータ型を管理UIから定義し、その中にテキストや画像、URLなどの任意のフィールドを自由に追加できます。

また、Viewsを使ってそれらをコレクション(一覧、グリッドなど)として抽出したり、REST APIとして公開することさえ、コードの開発を一切せずに実現できます。

これらの機能がフレームワークとして提供されることにより、開発者やサイト制作者は毎回繰り返し行われるような定型のタスクから解放され、ビジネスロジックやUXなど、本質的な価値を作り出すことに集中できるようになります。

ですが、「実際にデータベースの中がどのような構造になっているか」という解説は(特にDrupal 8では) あまりされていないようです。

そこで、Drupalの基本のコンテンツである「ノード」がどのようなデータ構造になっているか解説していこうと思います。

(この記事を作成時点の最新バージョンである 8.4.2 を元に調査しています)

インストール直後のテーブル一覧を見てみる

まず、Drupalのインストール直後にデータベースにどのようなテーブルがあるか見てみましょう。

MariaDB [drupal]> show tables;
+------------------------------+
| Tables_in_drupal             |
+------------------------------+
| block_content                |
| block_content__body          |
| block_content_field_data     |
| block_content_field_revision |
| block_content_revision       |
| block_content_revision__body |
| cache_bootstrap              |
| cache_config                 |
| cache_container              |
| cache_data                   |
| cache_default                |
| cache_discovery              |
| cache_dynamic_page_cache     |
| cache_entity                 |
| cache_menu                   |
| cache_page                   |
| cache_render                 |
| cachetags                    |
| comment                      |
| comment__comment_body        |
| comment_entity_statistics    |
| comment_field_data           |
| config                       |
| file_managed                 |
| file_usage                   |
| history                      |
| key_value                    |
| key_value_expire             |
| locale_file                  |
| locales_location             |
| locales_source               |
| locales_target               |
| menu_link_content            |
| menu_link_content_data       |
| menu_tree                    |
| node                         |
| node__body                   |
| node__comment                |
| node__field_image            |
| node__field_tags             |
| node_access                  |
| node_field_data              |
| node_field_revision          |
| node_revision                |
| node_revision__body          |
| node_revision__comment       |
| node_revision__field_image   |
| node_revision__field_tags    |
| queue                        |
| router                       |
| search_dataset               |
| search_index                 |
| search_total                 |
| semaphore                    |
| sequences                    |
| sessions                     |
| shortcut                     |
| shortcut_field_data          |
| shortcut_set_users           |
| taxonomy_index               |
| taxonomy_term_data           |
| taxonomy_term_field_data     |
| taxonomy_term_hierarchy      |
| url_alias                    |
| user__roles                  |
| user__user_picture           |
| users                        |
| users_data                   |
| users_field_data             |
| watchdog                     |
+------------------------------+
70 rows in set (0.01 sec)

たくさんありますね。。 今回のテーマはノードのデータ構造ということで、ノードの周辺だけ見ていきます。

ノード全体を管理する "node" テーブル

それでは、まずは "node" テーブルの定義を見てみましょう。

MariaDB [drupal]> describe node;
+----------+------------------+------+-----+---------+----------------+
| Field    | Type             | Null | Key | Default | Extra          |
+----------+------------------+------+-----+---------+----------------+
| nid      | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| vid      | int(10) unsigned | YES  | UNI | NULL    |                |
| type     | varchar(32)      | NO   | MUL | NULL    |                |
| uuid     | varchar(128)     | NO   | UNI | NULL    |                |
| langcode | varchar(12)      | NO   |     | NULL    |                |
+----------+------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

このテーブルのカラムにはそれぞれ以下のデータが格納されます。

"node" テーブルのデータ構造
カラム 格納されるデータ
nid

ノードのID

vid ノードのリビジョンID (後述)
type

コンテンツタイプ。「page」や「article」など。

uuid UUID。Drupal 7ではuuidモジュールを入れる必要がありましたが、Drupal 8ではコアでUUIDを生成してくれます(祝)。
langcode

フィールドの言語コード。多言語サイトでは言語毎にデータが管理されるため、en, jaなどの言語コードが入る。

Drupalのノードには、ノードを一意に特定するための "ノードID" の他に、保存する度に "リビジョン" と呼ばれるバージョンが付けられます。このようなデータ構造にすることで、過去の任意の時点のデータを表示したり、復元することが可能になります。

では、"/node/add" から最初から定義されている「基本ページ」のコンテンツを1件登録してみましょう。「基本ページ」のフィールドは「タイトル」と「本文」のみのシンプルな構成になっています。以下のように入力してみましょう。

「基本ページ」コンテンツの登録 (1)

 

無事に登録できるとこのような画面になります。

「基本ページ」コンテンツの登録 (2)

それでは、 "node" テーブルのデータを見てみましょう。

MariaDB [drupal]> select * from node;
+-----+------+------+--------------------------------------+----------+
| nid | vid  | type | uuid                                 | langcode |
+-----+------+------+--------------------------------------+----------+
|   1 |    1 | page | e56aa693-67ba-4b09-bcdc-00eeb9a1f581 | ja       |
+-----+------+------+--------------------------------------+----------+
1 row in set (0.00 sec)

先ほど作成した「基本ページ」のものらしきデータが入っています。 しかし、編集フォームでは "タイトル" や "本文" がありましたが、このテーブルには含まれていないことがわかります。

ノードのメタデータを管理する "node_field_data" テーブル

では、次に "node_field_data" テーブルの定義を見てみましょう。

MariaDB [drupal]> describe node_field_data;
+-------------------------------+------------------+------+-----+---------+-------+
| Field                         | Type             | Null | Key | Default | Extra |
+-------------------------------+------------------+------+-----+---------+-------+
| nid                           | int(10) unsigned | NO   | PRI | NULL    |       |
| vid                           | int(10) unsigned | NO   | MUL | NULL    |       |
| type                          | varchar(32)      | NO   | MUL | NULL    |       |
| langcode                      | varchar(12)      | NO   | PRI | NULL    |       |
| status                        | tinyint(4)       | NO   | MUL | NULL    |       |
| title                         | varchar(255)     | NO   | MUL | NULL    |       |
| uid                           | int(10) unsigned | NO   | MUL | NULL    |       |
| created                       | int(11)          | NO   | MUL | NULL    |       |
| changed                       | int(11)          | NO   | MUL | NULL    |       |
| promote                       | tinyint(4)       | NO   | MUL | NULL    |       |
| sticky                        | tinyint(4)       | NO   |     | NULL    |       |
| default_langcode              | tinyint(4)       | NO   |     | NULL    |       |
| revision_translation_affected | tinyint(4)       | YES  |     | NULL    |       |
+-------------------------------+------------------+------+-----+---------+-------+
13 rows in set (0.01 sec)

タイトル (title) がありましたね。こちらのテーブルにもnidとvidがあり、先程のnodeテーブルと結合できそうな感じがします。このテーブルのカラムにはそれぞれ以下のデータが格納されます。

"node_field_data" テーブルのデータ構造
カラム 格納されるデータ
nid

ノードのID

vid ノードのリビジョンID (後述)
type

コンテンツタイプ。「page」や「article」など。

langcode フィールドの言語コード。多言語サイトでは言語毎にデータが管理されるため、en, jaなどの言語コードが入る。
stats

ノードの掲載状態 (0が非掲載、1が掲載)

title ノードのタイトル
uid

ノードの作成者のユーザーID

created ノードの作成時刻
changed ノードの更新時刻
promote サイトのフロントページ(デフォルトのトップページ)に表示するかどうか
sticky 一覧表示した時にトップに表示するかどうか
default_language ノードのデフォルト言語

Drupal 7のデータ構造に詳しい方は、この時点でだいぶ構造が変わっていることに気がついたかもしれません。 Drupal 7ではstatusやtitleなどはnodeテーブル自体で管理されていました。 つまり、これらのデータはリビジョンを持っていませんでした。

Drupal 8では、これらの値もリビジョン毎に保存されるため、データのトレーサビリティがより向上しています。 では、こちらのデータも見ていきましょう。カラムが多いので必要なところだけに絞り込みます。

MariaDB [drupal]> select nid, vid, status, title from node_field_data;
+-----+-----+--------+------------------------------+
| nid | vid | status | title                        |
+-----+-----+--------+------------------------------+
|   1 |   1 |      1 | Hello Drupal Data Structure! |
+-----+-----+--------+------------------------------+
1 row in set (0.00 sec)

基本ページに入力したタイトルが入っていることが確認できました。

ノードのメタデータの履歴を管理する"node_field_revision" テーブル

さて、テーブル一覧に "node_field_revision" という似たような名前のテーブルがあります。 こちらのテーブルの定義も見てみましょう。

MariaDB [drupal]> describe node_field_revision;
+-------------------------------+------------------+------+-----+---------+-------+
| Field                         | Type             | Null | Key | Default | Extra |
+-------------------------------+------------------+------+-----+---------+-------+
| nid                           | int(10) unsigned | NO   | MUL | NULL    |       |
| vid                           | int(10) unsigned | NO   | PRI | NULL    |       |
| langcode                      | varchar(12)      | NO   | PRI | NULL    |       |
| status                        | tinyint(4)       | NO   |     | NULL    |       |
| title                         | varchar(255)     | YES  |     | NULL    |       |
| uid                           | int(10) unsigned | NO   | MUL | NULL    |       |
| created                       | int(11)          | YES  |     | NULL    |       |
| changed                       | int(11)          | YES  |     | NULL    |       |
| promote                       | tinyint(4)       | YES  |     | NULL    |       |
| sticky                        | tinyint(4)       | YES  |     | NULL    |       |
| default_langcode              | tinyint(4)       | NO   |     | NULL    |       |
| revision_translation_affected | tinyint(4)       | YES  |     | NULL    |       |
+-------------------------------+------------------+------+-----+---------+-------+
12 rows in set (0.00 sec)

typeカラムがないだけで、先ほどの "node_field_data" とほぼ同じ構造ですね。typeカラムがない理由は明確で、リビジョン毎に変わることがあり得ないからです。データも見てみましょう。

MariaDB [drupal]> select nid, vid, status, title from node_field_revision;
+-----+-----+--------+------------------------------+
| nid | vid | status | title                        |
+-----+-----+--------+------------------------------+
|   1 |   1 |      1 | Hello Drupal Data Structure! |
+-----+-----+--------+------------------------------+
1 row in set (0.01 sec)

先ほどと全く同じですね。無駄に思えますが、これにはどういう意味があるのでしょうか?試しに、作成したページのタイトルを「Hello Drupal Data Structure!」から「こんにちは、Drupalのデータ構造!」に書き換えてみましょう。

「基本ページ」コンテンツのタイトルを更新

ここでもう一度、node, node_field_data, node_field_revision をそれぞれ見てみます。

MariaDB [drupal]> select * from node;
+-----+------+------+--------------------------------------+----------+
| nid | vid  | type | uuid                                 | langcode |
+-----+------+------+--------------------------------------+----------+
|   1 |    2 | page | e56aa693-67ba-4b09-bcdc-00eeb9a1f581 | ja       |
+-----+------+------+--------------------------------------+----------+
1 row in set (0.00 sec)

MariaDB [drupal]> select nid, vid, status, title from node_field_data;
+-----+-----+--------+---------------------------------------------+
| nid | vid | status | title                                       |
+-----+-----+--------+---------------------------------------------+
|   1 |   2 |      1 | こんにちは、Drupalのデータ構造!             |
+-----+-----+--------+---------------------------------------------+
1 row in set (0.00 sec)

MariaDB [drupal]> select nid, vid, status, title from node_field_revision;
+-----+-----+--------+---------------------------------------------+
| nid | vid | status | title                                       |
+-----+-----+--------+---------------------------------------------+
|   1 |   1 |      1 | Hello Drupal Data Structure!                |
|   1 |   2 |      1 | こんにちは、Drupalのデータ構造!             |
+-----+-----+--------+---------------------------------------------+
2 rows in set (0.00 sec)

はい、だいぶ雰囲気が掴めてきましたね。

まず、 nodeテーブルのvidが1から2に変わっていることが分かります。node_field_data テーブルも同様にvidが1から2に変わっていて、タイトルも後で更新したときのデータになっていることが分かります。それに対して、node_field_revision テーブルには vidが1と2のデータが両方存在します。

つまり、

  • node_field_revision テーブルには過去の全てのリビジョンのデータが格納される
  • node_field_data テーブルには最新のリビジョンのデータのみが格納される
  • node テーブルのvidで最新のリビジョンを判断する

のようにデータが管理されていることになります。

「本文」フィールドのデータを管理する "node__body" テーブル

さて、少し分かってきたところで、次は「本文」のデータ構造も見ていきましょう。

MariaDB [drupal]> describe node__body;
+--------------+------------------+------+-----+---------+-------+
| Field        | Type             | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+-------+
| bundle       | varchar(128)     | NO   | MUL |         |       |
| deleted      | tinyint(4)       | NO   | PRI | 0       |       |
| entity_id    | int(10) unsigned | NO   | PRI | NULL    |       |
| revision_id  | int(10) unsigned | NO   | MUL | NULL    |       |
| langcode     | varchar(32)      | NO   | PRI |         |       |
| delta        | int(10) unsigned | NO   | PRI | NULL    |       |
| body_value   | longtext         | NO   |     | NULL    |       |
| body_summary | longtext         | YES  |     | NULL    |       |
| body_format  | varchar(255)     | YES  | MUL | NULL    |       |
+--------------+------------------+------+-----+---------+-------+
9 rows in set (0.00 sec)

body_value などはなんとなく意味がわかりますが、他はちょっとイメージがつかないですね。 データの方も見てみましょう。

MariaDB [drupal]> select bundle, entity_id, revision_id, body_value, body_format from node__body;
+--------+-----------+-------------+--------------------------------------------------------+-------------+
| bundle | entity_id | revision_id | body_value                                             | body_format |
+--------+-----------+-------------+--------------------------------------------------------+-------------+
| page   |         1 |           2 | <p>Drupalのデータ構造を見てみるよ!</p>                      | basic_html  |
+--------+-----------+-------------+--------------------------------------------------------+-------------+
1 row in set (0.00 sec)

コンテンツに入力した本文が入っているのが確認できました。 このテーブルのカラムにはそれぞれ以下のデータが格納されます。

"node__body" テーブルのデータ構造
カラム 格納されるデータ
bundle

エンティティの種別。ノードの場合は"page", "artcile" などのコンテンツタイプが格納される。

deleted コンテンツタイプ(正確にはbundle)からフィールドが削除された場合、1が格納される (通常は0)。
entity_id

エンティティのID。ノードの場合はノードIDが格納される。

revision_id エンティティのリビジョンID
langcode

フィールドの言語コード。多言語サイトでは言語毎にデータが管理されるため、en, jaなどの言語コードが入る。

delta

フィールドに値が複数入力可能な場合のインデックス番号。

「本文」フィールドのように1つしか入力できない場合は0が格納される。

body_value

「本文フィールド」の入力値

body_summary 「本文フィールド」(概要)の入力値
body_format

「本文フィールド」のテキストフォーマット。「basic_html」、「plaintext」、「markdown」など。

脱線: bundleとは

「bundle」という見慣れないキーワードが出てきました。ノードに限定するとそれほど重要な要素ではないのですが、Drupal全体のデータ構造を理解する上では非常に重要なので、少し脱線して説明します。

「bundle」を簡単に表現すると、「ある共通の定義や型を持ったデータの入れ物」です。Drupalのドキュメントでは、「In Drupal 8, bundles are a type of container for information that holds the field or setting definitions.」と表現されています。

ちなみに、これはDrupalの独自の用語ではありません。開発者であれば、フレームワークのソースコードやドキュメントなどで目にすることは多いでしょう。「bundle」が具体的に何を意味しているかはコンテキストによって若干変わりますが、Drupalでは、

  • ノードの場合: 「基本ページ」や「記事」など個々のコンテンツタイプがbundle
  • タクソノミーの場合: 個々の「ボキャブラリー」がbundle
  • ブロックの場合: 個々の「カスタムブロックタイプ」がbundle

のように使われます。

なぜ「bundle」という概念がデータ構造に取り入れられているかは、次回にまた解説します。

「本文」フィールドの履歴を管理する "node_revision__body" テーブル

さて、本文も例によってリビジョン毎にデータが管理されます。 "node_revision__body" のデータ構造を見てみましょう。

MariaDB [drupal]> describe node_revision__body;
+--------------+------------------+------+-----+---------+-------+
| Field        | Type             | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+-------+
| bundle       | varchar(128)     | NO   | MUL |         |       |
| deleted      | tinyint(4)       | NO   | PRI | 0       |       |
| entity_id    | int(10) unsigned | NO   | PRI | NULL    |       |
| revision_id  | int(10) unsigned | NO   | PRI | NULL    |       |
| langcode     | varchar(32)      | NO   | PRI |         |       |
| delta        | int(10) unsigned | NO   | PRI | NULL    |       |
| body_value   | longtext         | NO   |     | NULL    |       |
| body_summary | longtext         | YES  |     | NULL    |       |
| body_format  | varchar(255)     | YES  | MUL | NULL    |       |
+--------------+------------------+------+-----+---------+-------+

node__body と全く同じですね。 データも見てみましょう。

MariaDB [drupal]> select bundle, entity_id, revision_id, body_value, body_format from node_revision__body;
+--------+-----------+-------------+--------------------------------------------------------+-------------+
| bundle | entity_id | revision_id | body_value                                             | body_format |
+--------+-----------+-------------+--------------------------------------------------------+-------------+
| page   |         1 |           1 | <p>Drupalのデータ構造を見てみるよ!</p>                      | basic_html  |
| page   |         1 |           2 | <p>Drupalのデータ構造を見てみるよ!</p>                      | basic_html  |
+--------+-----------+-------------+--------------------------------------------------------+-------------+

タイトルと同様に、こちらもリビジョン毎にデータが独立して保存されているのが分かります。

まとめ

今回は、デフォルトで定義されている「基本ページ」のコンテンツを登録し、ノードと本文フィールドがどのようにデータとして管理されているかを見てみました。単にコンテンツをHTMLとして保存しているのではなく、バンドルやリビジョン、テキストフォーマットなど、データを抽象化し、構造化して管理するのがDrupalの大きな特徴です。

ちなみに、細かな違いはありますが、Drupal 7でも大枠のデータ構造はほぼ同様です。

次回はカスタムフィールドを追加し、そのデータ構造がどのようになっているかを解説する予定です。

(Photo by Thomas Kvistholt on Unsplash)

この記事を書いた人 : Yoshikazu Aoyama

ANNAI株式会社の出社しないCTO。
昔は回線交換やL2/L3のプロトコルスタックの開発をしてました。その後、組み込みLinuxやJava/Ruby on RailsなどのWebシステム開発などを経て現職。
インフラからDrupalのモジュール開発、Drupal以外の開発までなんでもやります。
普段は札幌で猫と一緒にリモートワークしています。 好きなモジュールは Restful Web Services と Rules

関連コンテンツ