Bis jetzt haben wir immer eine Datenbank direkt mit einer anderen
Datenbank verknüpft. Bei Many-to-Many werden wir zwei Datenbanken mithilfe
einer dritten Datenbank verknüpfen. Als Beispiel für eine solche Verknüpfung
nehmen wir eine Bestellung in einem Online-Shop. In einem solchen
Shop-System kann ein Produkt (Product
) in mehreren
Bestellungen (Order
) auftauchen und gleichzeitig kann
eine Bestellung mehrere Produkte enthalten. Das nennt sich dann
Many-to-Many. Am besten stellen wir das mal mit Code nach.
Die Verknüpfung funktioniert logischerweise wie has_many
Wir füllen als Erstes unsere Produktdatenbank mit folgenden
Werten:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > milch = Product.create(:name => 'Milch (1 Liter)', :price => 0.45)
(0.1ms) begin transaction
SQL (6.8ms) INSERT INTO "products" ("created_at", "name", "price", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:03:43 UTC +00:00], ["name", "Milch (1 Liter)"], ["price", #<BigDecimal:7fbbd4613db0,'0.45E0',9(45)>], ["updated_at", Mon, 07 May 2012 14:03:43 UTC +00:00]]
(1.1ms) commit transaction
=> #<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7fbbd3fe71d0,'0.45E0',9(45)>, created_at: "2012-05-07 14:03:43", updated_at: "2012-05-07 14:03:43">
1.9.3p194 :002 > butter = Product.create(:name => 'Butter (250 gr)', :price => 0.75)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "products" ("created_at", "name", "price", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:04:45 UTC +00:00], ["name", "Butter (250 gr)"], ["price", #<BigDecimal:7fbbd461d8d8,'0.75E0',9(45)>], ["updated_at", Mon, 07 May 2012 14:04:45 UTC +00:00]]
(2.5ms) commit transaction
=> #<Product id: 2, name: "Butter (250 gr)", price: #<BigDecimal:7fbbd4628b70,'0.75E0',9(45)>, created_at: "2012-05-07 14:04:45", updated_at: "2012-05-07 14:04:45">
1.9.3p194 :003 > mehl = Product.create(:name => 'Mehl (1 kg)', :price => 0.45)
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "products" ("created_at", "name", "price", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:05:26 UTC +00:00], ["name", "Mehl (1 kg)"], ["price", #<BigDecimal:7fbbd3ef3260,'0.45E0',9(45)>], ["updated_at", Mon, 07 May 2012 14:05:26 UTC +00:00]]
(5.7ms) commit transaction
=> #<Product id: 3, name: "Mehl (1 kg)", price: #<BigDecimal:7fbbd3ef6438,'0.45E0',9(45)>, created_at: "2012-05-07 14:05:26", updated_at: "2012-05-07 14:05:26">
1.9.3p194 :004 >
Jetzt legen wir ein neues
Order
-Objekt
bestellung
an:
1.9.3p194 :004 > bestellung = Order.new(:delivery_address => 'Bahnhofstr. 123, 12345 Beispielhausen')
=> #<Order id: nil, delivery_address: "Bahnhofstr. 123, 12345 Beispielhausen", created_at: nil, updated_at: nil>
1.9.3p194 :005 >
Logischerweise enthält diese neue Bestellung noch keine
Produkte:
1.9.3p194 :005 > bestellung.products
=> []
1.9.3p194 :006 >
Es gibt – wie so oft – verschiedene Arten, wie man Produkte zur
Bestellung hinzufügt. Der einfachste Weg: Da die Produkte als Array
eingebunden werden, kann man diese auch einfach als Elemente eines Arrays
einfügen:
1.9.3p194 :006 > bestellung.products << milch
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7fbbd3f3b5d8,'0.45E0',9(45)>, created_at: "2012-05-07 14:03:43", updated_at: "2012-05-07 14:03:43">]
1.9.3p194 :007 > bestellung.products
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7fbbd475c7d0,'0.45E0',9(45)>, created_at: "2012-05-07 14:03:43", updated_at: "2012-05-07 14:03:43">]
1.9.3p194 :008 >
Wenn der Käufer jetzt aber nicht einen Liter Milch, sondern drei
Liter Milch kaufen möchte, so müssen wir dies im
LineItem
(also im verknüpfenden Element) eintragen.
Dummerweise gibt es aber noch gar keinen
LineItem
zu dieser Bestellung:
1.9.3p194 :008 > bestellung.line_items
=> []
1.9.3p194 :009 >
Warum? Weil wir das Objekt noch nicht in der Datenbank gespeichert
haben. Wenn wir dies mit
save
machen, können wir
auch die Menge in den
line_items
verändern:
1.9.3p194 :009 > bestellung.save
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "orders" ("created_at", "delivery_address", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 07 May 2012 14:21:59 UTC +00:00], ["delivery_address", "Bahnhofstr. 123, 12345 Beispielhausen"], ["updated_at", Mon, 07 May 2012 14:21:59 UTC +00:00]]
SQL (0.5ms) INSERT INTO "line_items" ("created_at", "order_id", "product_id", "quantity", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:21:59 UTC +00:00], ["order_id", 1], ["product_id", 1], ["quantity", nil], ["updated_at", Mon, 07 May 2012 14:21:59 UTC +00:00]]
(3.6ms) commit transaction
=> true
1.9.3p194 :010 > bestellung.line_items
=> [#<LineItem id: 1, order_id: 1, product_id: 1, quantity: nil, created_at: "2012-05-07 14:21:59", updated_at: "2012-05-07 14:21:59">]
1.9.3p194 :011 > bestellung.line_items.first.update_attributes(:quantity => 3)
(0.1ms) begin transaction
(0.4ms) UPDATE "line_items" SET "quantity" = 3, "updated_at" = '2012-05-07 14:22:22.480585' WHERE "line_items"."id" = 1
(3.4ms) commit transaction
=> true
1.9.3p194 :012 > LineItem.first
LineItem Load (0.3ms) SELECT "line_items".* FROM "line_items" LIMIT 1
=> #<LineItem id: 1, order_id: 1, product_id: 1, quantity: 3, created_at: "2012-05-07 14:21:59", updated_at: "2012-05-07 14:22:22">
1.9.3p194 :013 >
Alternativ können wir aber auch direkt zweimal Butter kaufen, indem
wir ein
LineItem
hinzufügen:
1.9.3p194 :015 > bestellung.line_items.create(:product_id => butter.id, :quantity => 2)
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "line_items" ("created_at", "order_id", "product_id", "quantity", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:24:28 UTC +00:00], ["order_id", 1], ["product_id", 2], ["quantity", 2], ["updated_at", Mon, 07 May 2012 14:24:28 UTC +00:00]]
(3.5ms) commit transaction
=> #<LineItem id: 2, order_id: 1, product_id: 2, quantity: 2, created_at: "2012-05-07 14:24:28", updated_at: "2012-05-07 14:24:28">
1.9.3p194 :016 >
Warnung
Zu diesem Zeitpunkt dürfen Sie nicht den Fehler machen und denken,
dass das Objekt
bestellung
bereits über die bestellte
Butter verfügt. Schauen wir uns mal die
products
an:
1.9.3p194 :016 > bestellung.products
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7f820bf6b190,'0.45E0',9(45)>, created_at: "2012-05-07 14:20:05", updated_at: "2012-05-07 14:20:05">]
1.9.3p194 :017 >
Aber in der Datenbank ist es bereits richtig:
1.9.3p194 :017 > Order.first.products
Order Load (0.3ms) SELECT "orders".* FROM "orders" LIMIT 1
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "line_items" ON "products"."id" = "line_items"."product_id" WHERE "line_items"."order_id" = 1
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7f820ba849d8,'0.45E0',9(45)>, created_at: "2012-05-07 14:20:05", updated_at: "2012-05-07 14:20:05">, #<Product id: 2, name: "Butter (250 gr)", price: #<BigDecimal:7f820ba83a60,'0.75E0',9(45)>, created_at: "2012-05-07 14:20:31", updated_at: "2012-05-07 14:20:31">]
1.9.3p194 :018 >
Sie müssten in diesem konkreten Fall das Objekt mit der Methode
reload
neu aus der Datenbank laden:
1.9.3p194 :020 > bestellung.reload
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."id" = ? LIMIT 1 [["id", 1]]
=> #<Order id: 1, delivery_address: "Bahnhofstr. 123, 12345 Beispielhausen", created_at: "2012-05-07 14:21:59", updated_at: "2012-05-07 14:21:59">
1.9.3p194 :021 > bestellung.products
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "line_items" ON "products"."id" = "line_items"."product_id" WHERE "line_items"."order_id" = 1
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7f820bd4a410,'0.45E0',9(45)>, created_at: "2012-05-07 14:20:05", updated_at: "2012-05-07 14:20:05">, #<Product id: 2, name: "Butter (250 gr)", price: #<BigDecimal:7f820bd51468,'0.75E0',9(45)>, created_at: "2012-05-07 14:20:31", updated_at: "2012-05-07 14:20:31">]
1.9.3p194 :022 >
Geben wir eine zweite Bestellung mit allen verfügbaren Produkten ins
System:
1.9.3p194 :022 > bestellung2 = Order.create(:delivery_address => 'Teststr. 2')
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "orders" ("created_at", "delivery_address", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 07 May 2012 14:27:52 UTC +00:00], ["delivery_address", "Teststr. 2"], ["updated_at", Mon, 07 May 2012 14:27:52 UTC +00:00]]
(3.5ms) commit transaction
=> #<Order id: 2, delivery_address: "Teststr. 2", created_at: "2012-05-07 14:27:52", updated_at: "2012-05-07 14:27:52">
1.9.3p194 :023 > bestellung2.products << Product.all
Product Load (0.3ms) SELECT "products".* FROM "products"
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "line_items" ("created_at", "order_id", "product_id", "quantity", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:28:01 UTC +00:00], ["order_id", 2], ["product_id", 1], ["quantity", nil], ["updated_at", Mon, 07 May 2012 14:28:01 UTC +00:00]]
SQL (0.3ms) INSERT INTO "line_items" ("created_at", "order_id", "product_id", "quantity", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:28:01 UTC +00:00], ["order_id", 2], ["product_id", 2], ["quantity", nil], ["updated_at", Mon, 07 May 2012 14:28:01 UTC +00:00]]
SQL (0.2ms) INSERT INTO "line_items" ("created_at", "order_id", "product_id", "quantity", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 07 May 2012 14:28:01 UTC +00:00], ["order_id", 2], ["product_id", 3], ["quantity", nil], ["updated_at", Mon, 07 May 2012 14:28:01 UTC +00:00]]
(2.5ms) commit transaction
Product Load (0.2ms) SELECT "products".* FROM "products" INNER JOIN "line_items" ON "products"."id" = "line_items"."product_id" WHERE "line_items"."order_id" = 2
=> [#<Product id: 1, name: "Milch (1 Liter)", price: #<BigDecimal:7f820a1d6760,'0.45E0',9(45)>, created_at: "2012-05-07 14:20:05", updated_at: "2012-05-07 14:20:05">, #<Product id: 2, name: "Butter (250 gr)", price: #<BigDecimal:7f820a31ad10,'0.75E0',9(45)>, created_at: "2012-05-07 14:20:31", updated_at: "2012-05-07 14:20:31">, #<Product id: 3, name: "Mehl (1 kg)", price: #<BigDecimal:7f820a408e70,'0.45E0',9(45)>, created_at: "2012-05-07 14:20:48", updated_at: "2012-05-07 14:20:48">]
1.9.3p194 :024 > bestellung2.save
(0.1ms) begin transaction
(0.1ms) commit transaction
=> true
1.9.3p194 :025 >
Jetzt können wir die andere Seite der Many-to-Many-Verbindung
ausprobieren. Suchen wir mal nach allen Bestellungen, in denen das erste
Produkt enthalten ist:
1.9.3p194 :027 > Product.first.orders
Product Load (0.1ms) SELECT "products".* FROM "products" LIMIT 1
Order Load (0.2ms) SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "orders"."id" = "line_items"."order_id" WHERE "line_items"."product_id" = 1
=> [#<Order id: 1, delivery_address: "Bahnhofstr. 123, 12345 Beispielhausen", created_at: "2012-05-07 14:21:59", updated_at: "2012-05-07 14:21:59">, #<Order id: 2, delivery_address: "Teststr. 2", created_at: "2012-05-07 14:27:52", updated_at: "2012-05-07 14:27:52">]
1.9.3p194 :028 >
Natürlich können wir auch mit einem
joins
(siehe
„joins“) arbeiten und alle
Bestellungen, die das Produkt "Milch (1 Liter)" enthalten suchen:
1.9.3p194 :028 > Order.joins(:products).where(:products => {:name => 'Milch (1 Liter)'})
Order Load (0.3ms) SELECT "orders".* FROM "orders" INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id" INNER JOIN "products" ON "products"."id" = "line_items"."product_id" WHERE "products"."name" = 'Milch (1 Liter)'
=> [#<Order id: 1, delivery_address: "Bahnhofstr. 123, 12345 Beispielhausen", created_at: "2012-05-07 14:21:59", updated_at: "2012-05-07 14:21:59">, #<Order id: 2, delivery_address: "Teststr. 2", created_at: "2012-05-07 14:27:52", updated_at: "2012-05-07 14:27:52">]
1.9.3p194 :029 >
Mehr Informationen zu
has_many ... :through
finden Sie unter
???.