Posts 自己関連(自己結合)
Post
Cancel

自己関連(自己結合)

データベースのデータ間の関連を具体的に作成します。関連には、異なるテーブル間の関連以外に一つのテーブルのデータ間の関連いわゆる自己関連(SQLでは自己結合といわれています)というものがあります。

ここでは、自己関連を作りRails consoleで動作を確認します。

1対1関連については2モデル間の1:1関連を参照してください。

1対多関連については2モデル間の1:多関連を参照してください。

多対多関連については2モデル間の多:多関連を参照してください。

Model

今回、登場するモデルは、Materialモデルです。 テーブルでいえば、materialsテーブルです。

1モデルでの親子関係 -自己関連-

自己関連を「ファイル」を例として考えてみます。

元となるファイルがあります。そのファイルをコピーし、手を加えたものが複数できるというケースを考えてみます。

すなわち、親ファイル(parent)があって、1つの親ファイル(parent)から派生的に作成される子ファイル(children)がある、という状況です。parent : children は、1対多の関連になります。

親のファイル(parent)と子のファイル(children)はファイルという同じ範疇のものなので、1つのモデルで親子関係を実現することになります。そうすると親も子も同じモデルのインスタンスですから自分自身のモデル間の関連、自己関連になるわけです。子に関連付けた孫を作ることもできるので木構造を表現することができます。

ここではファイルという意味付けを行ないましたが、この図のノード(結節点)をディレクトリと思って見直してみるとディレクトリ構造を表しているように見ることもできます。

Materialsモデルの親子関連

Materialモデル に自己関連を作ります。

  • Materialモデルを作る

references を使ってフィールドを作ると、indexもつけてくれます。 Railsアプリケーションを作ったディレクトリで

$ rails g model material name:string material:references
$ rake db:migrate

を実行しモデルを作成します。

モデルに以下のような記述を加えます。ここで、material_idフィールド(material:referencesの指定で作成されました)には参照相手のidが入る訳ですが、そのidは「子」にとっての「親」のidになります。よって「子」の方にbelongs_toを記述します。反対の「親」の側には「1対多」関連なのでhas_manyを記述します。

  • material.rb
class Material < ActiveRecord::Base
  has_many :children, :class_name => "Material", :foreign_key => "material_id", dependent: :destroy
  belongs_to :parent, :class_name => "Material", :foreign_key => "material_id"
end

繰り返しになりますが、「親が子を(たくさん)持つ」なのでhas_many :childrenは「親」の側の指定です。「子が従属するのは親に」なのでbelongs_to :parentは「子」の側の指定です。

こうすることで、親からは「.children」で派生ファイル(の配列)に、子からは「.parent」で親ファイルにたどり着くことができるわけです。 クラス図はこのようになります。

また、親側にdependent: :destroyと記述することによって、親の削除に依存してその子供たちも一緒に削除されます。上の記述では子を削除しても親は(もちろんほかの子も)削除されません。

Rails console で確認

Rails console で確認してみます。

  1. 一つの親に子を3つ作り、その親の子を確認、うち一つの子の親を確認します。
  2. この一つをdestroyし、ほかに影響がないことを確かめます。
  3. 親を削除し、子がすべて(残っている2つ)がともに削除されることを確かめます。
  4. また孫を作り、「親」-「子」-「孫」の関係を確かめます。

では、確認していきます。

rails consoleコマンドに--sandboxオプションをつけると、終了時にデータベースに行った作業をロールバックして初めの状態に戻してくれます。

また、Hirbを起動して出力を見やすくしています。

$ rails console  --sandbox
Loading development environment in sandbox (Rails 4.1.8)
Any modifications you make will be rolled back on exit
irb(main):001:0> Hirb.enable
=> true

まず「親」を一つとその「子供たち」を3つ登録します。 コンソールに打ち込んだのは次のものです。

  1. par = Material.create(name: ‘親文書’)
  2. child1 = Material.create(name: ‘子文書’, parent: par)
  3. child2 = Material.create(name: ‘子文書2’, parent: par)
  4. child3 = Material.create(name: ‘子文書3’, parent: par)
  5. Material.all

1で親を作り、2~4で子を作っています。できたデータを5で確認しています。

実行結果です(途中は省略しています)。

irb(main):003:0> par = Material.create(name: '親文書')

SQL (0.3ms)  INSERT INTO "materials" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-04 08:41:53.085047"], ["name", "親文書"], ["updated_at", "2015-04-04 08:41:53.085047"]]

+----+--------+-------------+-------------------------+-------------------------+
| id | name   | material_id | created_at              | updated_at              |
+----+--------+-------------+-------------------------+-------------------------+
| 1  | 親文書 |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
+----+--------+-------------+-------------------------+-------------------------+
1 row in set
irb(main):004:0> child1 = Material.create(name: '子文書', parent: par)

SQL (0.2ms)  INSERT INTO "materials" ("created_at", "material_id", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-04 08:44:57.583292"], ["material_id", 1], ["name", "子文書"], ["updated_at", "2015-04-04 08:44:57.583292"]]

+----+--------+-------------+-------------------------+-------------------------+
| id | name   | material_id | created_at              | updated_at              |
+----+--------+-------------+-------------------------+-------------------------+
| 2  | 子文書 | 1           | 2015-04-04 08:44:57 UTC | 2015-04-04 08:44:57 UTC |
+----+--------+-------------+-------------------------+-------------------------+
1 row in set

    .....

irb(main):007:0> Material.all

Material Load (0.1ms)  SELECT "materials".* FROM "materials"

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 1  | 親文書   |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
| 2  | 子文書   | 1           | 2015-04-04 08:44:57 UTC | 2015-04-04 08:44:57 UTC |
| 3  | 子文書2 | 1           | 2015-04-04 08:45:13 UTC | 2015-04-04 08:45:13 UTC |
| 4  | 子文書3 | 1           | 2015-04-04 08:45:21 UTC | 2015-04-04 08:45:21 UTC |
+----+----------+-------------+-------------------------+-------------------------+
4 rows in set

では、親から3つの子を受け取ってみます。 親のインスタンスがparでしたのでpar.childrenが子供たちになります。

irb(main):008:0> par.children

Material Load (0.2ms)  SELECT "materials".* FROM "materials"  WHERE "materials"."material_id" = ?  [["material_id", 1]]

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 2  | 子文書   | 1           | 2015-04-04 08:44:57 UTC | 2015-04-04 08:44:57 UTC |
| 3  | 子文書2 | 1           | 2015-04-04 08:45:13 UTC | 2015-04-04 08:45:13 UTC |
| 4  | 子文書3 | 1           | 2015-04-04 08:45:21 UTC | 2015-04-04 08:45:21 UTC |
+----+----------+-------------+-------------------------+-------------------------+
3 rows in set

3つの子供たちが受け取れています。

では、子供から親を取得してみます。 子child1の親は、child1.parentです。

irb(main):009:0> child1.parent

+----+--------+-------------+-------------------------+-------------------------+
| id | name   | material_id | created_at              | updated_at              |
+----+--------+-------------+-------------------------+-------------------------+
| 1  | 親文書 |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
+----+--------+-------------+-------------------------+-------------------------+
1 row in set

親が受け取れました。 親のnameなら直接取得できます。

irb(main):010:0> child1.parent.name
=> "親文書"

では、子を一つ削除します。 削除し、確認するために次を打ち込みます。

  1. child2.destroy
  2. Material.all

その結果です。

irb(main):011:0> child2.destroy

  Material Load (0.1ms)  SELECT "materials".* FROM "materials"  WHERE "materials"."material_id" = ?  [["material_id", 3]]
  SQL (0.1ms)  DELETE FROM "materials" WHERE "materials"."id" = ?  [["id", 3]]

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 3  | 子文書2 | 1           | 2015-04-04 08:45:13 UTC | 2015-04-04 08:45:13 UTC |
+----+----------+-------------+-------------------------+-------------------------+
1 row in set
irb(main):012:0> Material.all

Material Load (0.1ms)  SELECT "materials".* FROM "materials"

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 1  | 親文書   |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
| 2  | 子文書   | 1           | 2015-04-04 08:44:57 UTC | 2015-04-04 08:44:57 UTC |
| 4  | 子文書3 | 1           | 2015-04-04 08:45:21 UTC | 2015-04-04 08:45:21 UTC |
+----+----------+-------------+-------------------------+-------------------------+
3 rows in set

真ん中の子がいなくなりました。

いよいよ、親を削除します。確認のためこの親とは無関係なデータを加えてから親を削除することにします。

削除を確認するために次を打ち込みます。

  1. Material.all
  2. par.destroy
  3. Material.all

その結果です。

irb(main):016:0> Material.all

Material Load (0.1ms)  SELECT "materials".* FROM "materials"

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 1  | 親文書   |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
| 2  | 子文書   | 1           | 2015-04-04 08:44:57 UTC | 2015-04-04 08:44:57 UTC |
| 4  | 子文書3 | 1           | 2015-04-04 08:45:21 UTC | 2015-04-04 08:45:21 UTC |
| 5  | 親2     |             | 2015-04-04 09:35:50 UTC | 2015-04-04 09:35:50 UTC |
| 6  | 親2の子 | 5           | 2015-04-04 09:36:58 UTC | 2015-04-04 09:36:58 UTC |
+----+----------+-------------+-------------------------+-------------------------+
5 rows in set
irb(main):017:0> par.destroy

  Material Load (0.1ms)  SELECT "materials".* FROM "materials"  WHERE "materials"."material_id" = ?  [["material_id", 2]]
  SQL (0.1ms)  DELETE FROM "materials" WHERE "materials"."id" = ?  [["id", 2]]
  Material Load (0.0ms)  SELECT "materials".* FROM "materials"  WHERE "materials"."material_id" = ?  [["material_id", 3]]
  SQL (0.1ms)  DELETE FROM "materials" WHERE "materials"."id" = ?  [["id", 3]]
  Material Load (0.0ms)  SELECT "materials".* FROM "materials"  WHERE "materials"."material_id" = ?  [["material_id", 4]]
  SQL (0.0ms)  DELETE FROM "materials" WHERE "materials"."id" = ?  [["id", 4]]
  SQL (0.0ms)  DELETE FROM "materials" WHERE "materials"."id" = ?  [["id", 1]]

+----+--------+-------------+-------------------------+-------------------------+
| id | name   | material_id | created_at              | updated_at              |
+----+--------+-------------+-------------------------+-------------------------+
| 1  | 親文書 |             | 2015-04-04 08:41:53 UTC | 2015-04-04 08:41:53 UTC |
+----+--------+-------------+-------------------------+-------------------------+
1 row in set
irb(main):018:0> Material.all

  Material Load (0.1ms)  SELECT "materials".* FROM "materials"

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 5  | 親2     |             | 2015-04-04 09:35:50 UTC | 2015-04-04 09:35:50 UTC |
| 6  | 親2の子 | 5           | 2015-04-04 09:36:58 UTC | 2015-04-04 09:36:58 UTC |
+----+----------+-------------+-------------------------+-------------------------+
2 rows in set

親を削除すると、親とその親に紐付いた子供たちが削除されました。

そうでした、孫も作れますね。 子供を親にした子が孫になります。孫 = Material.create(name: '孫', parent: 子のインスタンス)です。

孫から親を引くには孫.parent 孫から祖父を引くには孫.parent.parentでできそうですね。

irb(main):019:0> mago = Material.create(name: '孫', parent: child_of_oya2)

  SQL (0.1ms)  INSERT INTO "materials" ("created_at", "material_id", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-04 09:44:46.117392"], ["material_id", 6], ["name", "孫"], ["updated_at", "2015-04-04 09:44:46.117392"]]

+----+------+-------------+-------------------------+-------------------------+
| id | name | material_id | created_at              | updated_at              |
+----+------+-------------+-------------------------+-------------------------+
| 7  |    | 6           | 2015-04-04 09:44:46 UTC | 2015-04-04 09:44:46 UTC |
+----+------+-------------+-------------------------+-------------------------+
1 row in set
irb(main):020:0> mago.parent

+----+----------+-------------+-------------------------+-------------------------+
| id | name     | material_id | created_at              | updated_at              |
+----+----------+-------------+-------------------------+-------------------------+
| 6  | 親2の子 | 5           | 2015-04-04 09:36:58 UTC | 2015-04-04 09:36:58 UTC |
+----+----------+-------------+-------------------------+-------------------------+
1 row in set
irb(main):021:0> mago.parent.parent

+----+------+-------------+-------------------------+-------------------------+
| id | name | material_id | created_at              | updated_at              |
+----+------+-------------+-------------------------+-------------------------+
| 5  | 親2 |             | 2015-04-04 09:35:50 UTC | 2015-04-04 09:35:50 UTC |
+----+------+-------------+-------------------------+-------------------------+
1 row in set

なんて簡単!!やっぱりRailsはすばらしい。

シェア
#内容発言者

バリデートメソッドを自作する1

2モデル間の1:1関連