2009-05-19 21 views
28
create_table :categories_posts, :id => false do |t| 
    t.column :category_id, :integer, :null => false 
    t.column :post_id, :integer, :null => false 
end 

Ich habe eine Tabelle verbinden (wie oben) mit Spalten, die zu einer entsprechenden Kategorien Tabelle beziehen und ein Beiträge Tisch. Ich wollte eine eindeutige Einschränkung auf der Verbundschlüssel Kategorie category_id, post_id in der categories_posts Join-Tabelle erzwingen. Aber Rails unterstützt das nicht (glaube ich).Was ist in einer Join-Tabelle die beste Problemumgehung dafür, dass Rails keinen zusammengesetzten Schlüssel hat?

Um die Möglichkeit für doppelte Zeilen in meinen Daten mit der gleichen Kombination von category_id und post_id, zu vermeiden, was ist die beste Problemumgehung für die Abwesenheit eines zusammengesetzten Schlüssels in Rails?

Meine Annahmen sind hier:

  1. Die Standard-Auto-Nummernspalte (id: integer) würde nichts tun, um meine Daten in dieser Situation zu schützen.
  2. ActiveScaffold kann eine Lösung zur Verfügung stellen, aber ich bin nicht sicher, ob es übertreibt es einfach für diese einzelne Eigenschaft in mein Projekt einzubeziehen ist, vor allem, wenn es eine elegantere Antwort.

Antwort

33

Fügen Sie einen eindeutigen Index hinzu, der beide Spalten enthält. Das verhindert, dass Sie einen Datensatz einfügen, der ein doppeltes category_id/post_id-Paar enthält.

add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => 'by_category_and_post' 
+0

Danke. Beim Lesen verschiedener Blogposts dachte ich, ein zusammengesetzter Index sei nicht möglich und diese Syntax existiere nicht. –

+0

Dies führt zu einer schlechten Benutzeroberflächenerfahrung, wenn der Benutzer versucht, ein dupliziertes rec einzugeben. –

+1

@Larry - Konnte ich die Validierungslogik aus Ihrer Antwort immer noch verwenden und mit dieser Rails-Syntax für zusammengesetzte Indizes kombinieren? –

5

ich die beiden folgenden implementieren, wenn ich dieses Problem in Schienen haben:

1) Sie sollten einen einzigartigen Composite-Index auf Datenbankebene deklariert haben, um sicherzustellen, dass die dbms nicht ein Duplikat lassen Datensatz wird erstellt.

2) glatter Fehler msgs Um als nur die oben, eine Validierung an die Rails-Modell hinzufügen:

validates_each :category_id, :on => :create do |record, attr, value| 
    c = value; p = record.post_id 
    if c && p && # If no values, then that problem 
       # will be caught by another validator 
    CategoryPost.find_by_category_id_and_post_id(c, p) 
    record.errors.add :base, 'This post already has this category' 
    end 
end 
+0

Entsprechend tvanoffsons Antwort ist ein zusammengesetzter Index tatsächlich möglich und er hat die Syntax dafür zur Verfügung gestellt. In diesem Fall wäre Ihr Vorschlag, den zusammengesetzten Index auf Datenbankebene zu deklarieren (während ein guter Vorschlag), unnötig. Meine Annahme war, dass zusammengesetzte Indizes in Rails nicht möglich waren, aber vielleicht war ich falsch. –

+2

Tvanfossons Lösung ist die gleiche wie meine # 1 - der Index wird auf der db-Ebene deklariert, nicht auf der Rails- oder Ruby-Ebene. Rails können nur einen "SQL-Fehler" melden, wenn ein dup gesendet wird. Deshalb möchten Sie auch im Modell validieren (auf der Rails-Ebene). –

+0

Wenn Sie "auf Datenbankebene" sagten, dachten Sie, Sie wollten es in der Datenbank erstellen, ohne die Methode add_index in Rails zu verwenden. Der Grund für meine Frage war, dass ich keine zusammengesetzten Indizes kannte. Aber ich verstehe nicht, warum ich Ihre Validierungslogik nicht aus tvanoffsons Antwort zur Syntax hinzufügen kann. Entschuldigung, wenn Sie das beabsichtigt haben. Aber das habe ich aus deiner Antwort nicht verstanden. –

9

Ich glaube, Sie leichter finden können Einzigartigkeit eines der Felder mit dem anderen als zu validieren Umfang:

von der API:

validates_uniqueness_of(*attr_names) 

Prüft, ob der Wert der angegebenen Attribute im gesamten System eindeutig ist. Nützlich, um sicherzustellen, dass nur ein Benutzer "davidhh" genannt werden kann.

class Person < ActiveRecord::Base 
    validates_uniqueness_of :user_name, :scope => :account_id 
    end 

Es kann auch bestätigen, ob der Wert der angegebenen Attribute sind einzigartig auf der Basis mehrerer Umfang Parameter. Zum Beispiel, sicherzustellen, dass ein Lehrer nur einmal pro Semester für eine bestimmte Klasse im Zeitplan sein kann.

class TeacherSchedule < ActiveRecord::Base 
    validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] 
    end 

Wenn der Datensatz erstellt wird, wird geprüft, um sicherzustellen, durchgeführt, dass kein Datensatz mit dem angegebenen Wert für das angegebene Attribut in der Datenbank vorhanden ist (dh Karten auf eine Spalte). Wenn der Datensatz aktualisiert wird, wird dieselbe Überprüfung durchgeführt, aber der Datensatz selbst wird ignoriert.

Optionen Konfiguration:

* message - Specifies a custom error message (default is: "has already been taken") 
* scope - One or more columns by which to limit the scope of the uniquness constraint. 
* case_sensitive - Looks for an exact match. Ignored by non-text columns (true by default). 
* allow_nil - If set to true, skips this validation if the attribute is null (default is: false) 
* if - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The method, proc or string should return or evaluate to a true or false value. 
+0

Das ist großartig. Vielen Dank. –

+0

wie @izap sagte in seiner Antwort: Die Validierung ist nicht idiotensicher wegen einer möglichen Race-Bedingung zwischen den Abfragen SELECT und INSERT/UPDATE. –

1

Eine Lösung kann sowohl den Index und die Validierung des Modells hinzuzufügen sein.

So in der Migration Sie haben: add_index: categories_posts, [: category_id,: post_id]: unique => true

Und im Modell: validates_uniqueness_of: category_id,: scope => [: category_id ,: post_id] validates_uniqueness_of: post_id,: scope => [: category_id,: post_id]

8

Es ist sehr hart den "richtigen" Ansatz zu empfehlen.

1) Der pragmatische Ansatz

Verwenden Validator und fügen Sie keine einzigartigen Composite-Index. Dies gibt Ihnen nette Nachrichten in der Benutzeroberfläche und es funktioniert einfach.

class CategoryPost < ActiveRecord::Base 
    belongs_to :category 
    belongs_to :post 

    validates_uniqueness_of :category_id, :scope => :post_id, :message => "can only have one post assigned" 
end 

Sie können auch in Ihrer beitreten Tabelle zwei separate Indizes hinzufügen Suche zu beschleunigen:

add_index :categories_posts, :category_id 
add_index :categories_posts, :post_id 

Bitte beachten Sie (nach dem Buch Rails 3 Way) die Validierung nicht narrensicher ist, weil einer möglichen Race-Bedingung zwischen den Abfragen SELECT und INSERT/UPDATE. Es wird empfohlen, eine eindeutige Einschränkung zu verwenden, wenn Sie absolut sicher sein müssen, dass keine doppelten Datensätze vorhanden sind.

2) Der kugelsicher Ansatz

In diesem Ansatz, den wir eine Einschränkung auf Datenbankebene setzen wollen. So bedeutet es einen zusammengesetzten Index zu erstellen:

add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => 'by_category_and_post' 

Ein großer Vorteil ist eine große Datenbank-Integrität, Nachteil nicht viel nützlicher Fehler ist für den Benutzer berichten. Bitte beachten Sie, dass beim Erstellen eines zusammengesetzten Index die Reihenfolge der Spalten wichtig ist.

Wenn Sie weniger selektive Spalten als führende Spalten im Index platzieren und die meisten selektiven Spalten am Ende platzieren, können andere Abfragen, die eine Bedingung für nicht führende Indexspalten haben, auch INDEX SKIP SCAN nutzen. Möglicherweise müssen Sie einen weiteren Index hinzufügen, um sie zu nutzen, aber dies ist stark abhängig von der Datenbank.

3) Kombination beider

Man kann über Kombination von beiden lesen, aber ich neige dazu, die Nummer eins nur zu mögen.

+0

Dies sollte als der beste Abswer betrachtet werden, da es die Modell- und Datenintegrität vorschlägt. –

+0

Ich stimme nicht mit der Empfehlung, nur die Nummer eins zu verwenden, aber da alle Vor- und Nachteile erwähnt werden, verdient dies noch eine Aufwertung. – kronn