わかりやすいJPA(3): INNER JOIN

はじめに

これは、JPAについて解説する連載の3回目です。

SELECT文の中でFROMは検索対象とするエンティティを指定する部分ですが、FROM句に複数のエンティティを指定するパターンは重要です。FROM句に複数のエンティティを指定すると、原則として、各エンティティ同士のすべての可能な組み合わせを得ることができます。

その際、組み合わせ方を指定するために、JOINオペレータが使われます。JOINオペレータによる操作はリレーショナルデータベースに特有の操作です。

そこで、この章では、手始めとして、FROM句の書き方やJOIN(INNER JOIN)オペレータの使い方について詳しく解説します。

解説で出てくるエンティティやクエリの実行については、「連載:わかりやすいJPA」の予備知識 を参照してください。

FROM句に複数のエンティティを指定する

まずは、単純にFROM句に複数のエンティティを指定してみましょう。SELECT文をJpqlTestで実行して結果を確認します。

 SELECT d,e     FROM Department d, Employee e

(結果は省略。多くなりすぎるのでJpqlTestを実行して確認してください)

すべてのDepartmentエンティティと、すべてのEmployeeエンティティの可能な組み合わせがリストアップされるはずです。Departmentエンティティは8件、Employeeエンティティは13件あるので、8×13=104件 の結果が出力されます。

ついでに次のSELECT文も実行してみてください。

 SELECT e1, e2  FROM Employee e1, Employee e2

(結果は省略。多くなりすぎるのでJpqlTestを実行して確認してください)

Employeeエンティティは13件あるので、その組み合わせから13×13=169件 の結果が出力されます。

集合論のコトバではこのような演算を直積(またはデカルト積)といいます。ただ、結果を直積で得ると多くなりすぎるので、これにいろいろな条件を付けて絞り込んだ結果を得るために使います。

例えば、WHERE句に次のように指定すると、出力は大分少なくなります。これは、すべての社員e1について、e1よりも低い給与の、すべての社員e2との組み合わせを出力します。

 SELECT e1, e2  FROM Employee e1, Employee e2 
 where e1.salary > e2.salary

このように条件はWHERE句に記述するのがわかりやすいのですが、FROM句でも、すこし違った方法で、検索結果を狭い範囲に限定できます。

INNER JOIN  ー エンティティとそのCollectionメンバを使う

Employeeクラスは、phonesというメンバを持っています。phonesはその社員が持つ電話のリストです。そのため、phonesは、電話クラスPhoneのコレクション(リスト)として、

Collection<Phone> phones;

と定義されています。

つまりphonesは、『Phoneエンティティの集り』です。

そこで、FROM句で、Employeeエンティティと、この『Phoneエンティティの集り』(=phones)を指定すると、そのすべての組み合わせ(直積)を出力させることができます。つまり、各Employeeエンティティごとに、それが持つ『Phoneエンティティの集まり』(=phones)との直積を出力させるわけです。

このような場合に使うのがJOINオペレータで、次のように書きます。

 SELECT e, p FROM Employee e  JOIN e.phones p

JOIN e.phones p の変数pはJOIN変数といい、e.phonesではなくe.phones の要素を表すことに注意してください。つまり、e.phonesから取り出すPhone型エンティティを表すエイリアスです。これは、JOIN (e.phones)  p のように括弧でくくって考えるとわかりやすいかもしれません。

実行結果は、次のように、各 e ごとに、それが持つpとのすべての組み合わせ(直積)になります。実際の出力を次で確認してください。

例えば、先頭の2件は、employeeの id が1ですが、Phoneの idはそれぞれ 1 と 2 になっています。これは、id=1のEmployeeエンティティが2つの電話(id=1,2)を持っているからです。Employee(id=1)×Phone(id=1, 2)の直積から、2件のデータが出力されています。他の出力についても、Employeeエンティティの id と、Phoneエンティティの id を見ると、どのような組み合わせで出力されたか確認できます。

確かに直積になっていることがわかるでしょう。

※Employeeの出力表示は見やすくするために名前以下を省略しています
[Employee [id: 1, name: John, .......], Phone [id: 1, no: (212)555-1234, type: Office,employee_Id: 1]]
[Employee [id: 1, name: John, .......], Phone [id: 2, no: (212)555-9843, type: Home,employee_Id: 1]]
[Employee [id: 2, name: Rob, ........], Phone [id: 3, no: (315)555-6253, type: Office,employee_Id: 2]]
[Employee [id: 3, name: Peter, ......], Phone [id: 4, no: (516)555-9837, type: Office,employee_Id: 3]]
[Employee [id: 3, name: Peter, ......], Phone [id: 5, no: (516)555-2034, type: Cell,employee_Id: 3]]
[Employee [id: 4, name: Frank, ......], Phone [id: 6, no: (650)555-7583, type: Office,employee_Id: 4]]
[Employee [id: 4, name: Frank, ......], Phone [id: 7, no: (650)555-5345, type: Home,employee_Id: 4]]
[Employee [id: 5, name: Scott, ......], Phone [id: 8, no: (650)555-9386, type: Office,employee_Id: 5]]
[Employee [id: 5, name: Scott, ......], Phone [id: 9, no: (650)555-4885, type: Cell,employee_Id: 5]]
[Employee [id: 6, name: Sue, ........], Phone [id: 10, no: (650)555-3836, type: Office,employee_Id: 6]]
[Employee [id: 6, name: Sue, ........], Phone [id: 11, no: (650)555-0985, type: Home,employee_Id: 6]]
[Employee [id: 6, name: Sue, ........], Phone [id: 12, no: (650)555-1946, type: Cell,employee_Id: 6]]
[Employee [id: 7, name: Stephanie, ..], Phone [id: 13, no: (650)555-4759, type: Office,employee_Id: 7]]
[Employee [id: 7, name: Stephanie, ..], Phone [id: 14, no: (650)555-4757, type: Home,employee_Id: 7]]
[Employee [id: 8, name: Jennifer, ...], Phone [id: 15, no: (650)555-6753, type: Office,employee_Id: 8]]
[Employee [id: 9, name: Sarah, ......], Phone [id: 16, no: (585)555-0693, type: Office,employee_Id: 9]]
[Employee [id: 9, name: Sarah, ......], Phone [id: 17, no: (585)555-3098, type: Home,employee_Id: 9]]
[Employee [id: 9, name: Sarah, ......], Phone [id: 18, no: (585)555-1457, type: Cell,employee_Id: 9]]
[Employee [id: 10, name: Joan, ......], Phone [id: 19, no: (650)555-9838, type: Office,employee_Id: 10]]
[Employee [id: 10, name: Joan, ......], Phone [id: 20, no: (650)555-2346, type: Home,employee_Id: 10]]
[Employee [id: 10, name: Joan, ......], Phone [id: 21, no: (650)555-9874, type: Cell,employee_Id: 10]]

このような、エンティティとそのメンバであるコレクションとの直積は、INNER JOIN(内部結合)といいます。

INNER JOINはNULL値をスキップする

ところで、Employeeエンティティは id=1~13まで、13件あるのですが、上の結果ではid=1~10までしか出力されていません。なぜでしょうか。

実は、Employeeエンティティのうち、id=11,12,13のデータは、phoneが null になっています。INNER JOINでは、NULLである変数(=p)は処理対象とせずスキップします。そのためid=11~13のデータが含まれないのです。

確認のため、次のSELECT文を実行して、id=11~13 のデータの phones の値を見ましょう。phonesはリストなので、出力ではリストの要素数だけをphones_Sz:2のように表示しています。phonesがNULLの場合、phones_Sz: ***と表示されます。

id=11~13は***が表示されているので、NULLであることが分かりますね。

 SELECT  e FROM Employee e
[ Employee [id: 1, name: John, salary: 55000, address_Id: 1, phones_Sz: 2, manager_Id: 9, department_Id: 2, directs_Sz: ***, projects_Sz: 1] ]
[ Employee [id: 2, name: Rob, salary: 53000, address_Id: 2, phones_Sz: 1, manager_Id: 9, department_Id: 2, directs_Sz: ***, projects_Sz: 2] ]
[ Employee [id: 3, name: Peter, salary: 40000, address_Id: 3, phones_Sz: 2, manager_Id: 9, department_Id: 2, directs_Sz: ***, projects_Sz: 3] ]
[ Employee [id: 4, name: Frank, salary: 41000, address_Id: 4, phones_Sz: 2, manager_Id: 10, department_Id: 1, directs_Sz: ***, projects_Sz: 1] ]
[ Employee [id: 5, name: Scott, salary: 60000, address_Id: 5, phones_Sz: 2, manager_Id: 10, department_Id: 1, directs_Sz: ***, projects_Sz: 2] ]
[ Employee [id: 6, name: Sue, salary: 62000, address_Id: 6, phones_Sz: 3, manager_Id: 10, department_Id: 1, directs_Sz: ***, projects_Sz: 2] ]
[ Employee [id: 7, name: Stephanie, salary: 54000, address_Id: 7, phones_Sz: 2, manager_Id: 10, department_Id: 1, directs_Sz: ***, projects_Sz: 1] ]
[ Employee [id: 8, name: Jennifer, salary: 45000, address_Id: 8, phones_Sz: 1, manager_Id: ***, department_Id: 1, directs_Sz: ***, projects_Sz: 2] ]
[ Employee [id: 9, name: Sarah, salary: 52000, address_Id: 9, phones_Sz: 3, manager_Id: 10, department_Id: 2, directs_Sz: 3, projects_Sz: 2] ]
[ Employee [id: 10, name: Joan, salary: 59000, address_Id: 10, phones_Sz: 3, manager_Id: ***, department_Id: 1, directs_Sz: 5, projects_Sz: 3] ]
[ Employee [id: 11, name: Marcus, salary: 35000, address_Id: ***, phones_Sz: ***, manager_Id: ***, department_Id: ***, directs_Sz: 1, projects_Sz: ***] ]
[ Employee [id: 12, name: Joe, salary: 36000, address_Id: ***, phones_Sz: ***, manager_Id: 11, department_Id: 3, directs_Sz: ***, projects_Sz: ***] ]
[ Employee [id: 13, name: Jack, salary: 43000, address_Id: ***, phones_Sz: ***, manager_Id: ***, department_Id: 3, directs_Sz: ***, projects_Sz: ***] ]

RDBの世界ではテーブル間の操作になる

オブジェクト指向の世界観で、データベースを操作できるのがJPAの利点ですが、どんな操作もSQLに翻訳され、RDBの世界で動くことを忘れないようにしましょう。

「わかりやすいJavaEE」の17章で解説したORM(オブジェクト関係マッピング)を思いだしてください。エンティティオブジェクトは、その中にコレクションや他のオブジェクトを持つことができますが、コレクションや他のオブジェクトは、どれも、別のデータベーステーブルに作成され、外部キーや結合テーブルのような仕組みで、関係をマップしているにすぎません。

したがって、JOIN操作は、データベーステーブル同士の操作になります。つまり、二つ以上のデータベーステーブルから、それらの仮想的な結合テーブルを作り出すのがJOIN操作なのです。

JOIN操作を実行するには、単なる検索以上にメモリーやCPUパワーが必要になることを覚えておきましょう。また、場合によっては、多数のSQLが発行されるケースもあります。SQLの発行を少なくする方法は、この後で解説しますが、常にログを監視して、どのようなSQLが発行されているか見ておくことも大切です。

読者になる

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

%d人のブロガーが「いいね」をつけました。