2009-05-19 18 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 

Ho una tabella join (come sopra) con colonne che fanno riferimento ad un corrispondente categorie tavolo e un post tabella. Volevo applicare un vincolo univoco su alla chiave composita ID categoria, post_id nella tabella di join categories_posts. Ma Rails non supporta questo (credo).In una tabella di join, qual è la soluzione migliore per l'assenza di una chiave composita di Rails?

Per evitare il rischio di righe duplicate nei miei dati con la stessa combinazione di id_categoria e post_id, qual è la soluzione migliore per l'assenza di una chiave composita in Rails?

mie ipotesi sono qui:

  1. L'auto-numero predefinito di colonna (id: intero) non avrebbe fatto nulla per proteggere i miei dati in questa situazione.
  2. ActiveScaffold può fornire una soluzione ma non sono sicuro se è eccessivo per includerlo nel mio progetto semplicemente per questa singola funzione, soprattutto se v'è un risposta più elegante.

risposta

33

Aggiungere un indice univoco che include entrambe le colonne. Ciò impedirà l'inserimento di un record che contiene una coppia duplicata category_id/post_id.

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

Grazie. Dalla lettura di vari post del blog ho pensato che un indice composito non fosse possibile e che questa sintassi non esistesse. –

+0

Questo darà una scarsa esperienza di interfaccia utente se l'utente tenta di inserire un duplicato rec. –

+1

@Larry - Non posso ancora utilizzare la logica di convalida dalla tua risposta e combinarla con questa sintassi di Rails per gli indici compositi? –

5

I implementare entrambe le seguenti quando ho questo problema in rotaie:

1) Si dovrebbe avere un indice composito unico dichiarato a livello di database per garantire che i DBMS non permetterà che un duplicato registrare ottenere creato.

2) Fornire msg di errore più morbide che solo quanto sopra, aggiungere una convalida al modello Rails:

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

Secondo la risposta di tvanoffson, un indice composito è effettivamente possibile e ha fornito la sintassi per esso. In tal caso, il tuo suggerimento di dichiarare l'indice composito a livello di database (mentre un buon suggerimento) non sarebbe necessario. La mia ipotesi era che gli indici compositi non fossero possibili in Rails ma forse non ero corretto. –

+2

La soluzione di Tvanfosson è la stessa del mio n. 1 - l'indice è dichiarato al livello db, non al livello Rails o Ruby. Rails sarà in grado di segnalare un "errore SQL" solo quando viene inviato un dup. Ecco perché vuoi anche convalidare nel modello (a livello di Rails). –

+0

Quando hai detto "a livello di database" pensavo volessi crearlo nel database senza utilizzare il metodo add_index in Rails. Il motivo della mia domanda era che non sapevo che gli indici compositi fossero possibili. Ma non vedo perché non riesco ad aggiungere la tua logica di convalida alla sintassi dalla risposta di tvanoffson. Scusa se è quello che intendevi. Ma non l'ho capito dalla tua risposta. –

9

Penso che si può trovare più facile per convalidare l'unicità di uno dei campi con l'altro come una portata:

dELLA API:

validates_uniqueness_of(*attr_names) 

Convalida se il valore degli attributi specificati sono unici in tutto il sistema. Utile per assicurarsi che solo un utente possa essere chiamato "davidhh".

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

Può inoltre verificare se il valore degli attributi specificati è univoco in base a più parametri di ambito. Ad esempio, assicurandosi che un insegnante possa essere presente solo una volta al semestre per una determinata classe.

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

Quando viene creato il record, viene effettuato un controllo per verificare che nessun record esiste nel database con il valore dato per l'attributo specificato (che associa a una colonna). Quando il record viene aggiornato, viene eseguito lo stesso controllo ma ignorando il record stesso.

Le opzioni di configurazione:

* 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

Questo è fantastico. Grazie. –

+0

come ha detto @izap nella sua risposta: la convalida non è infallibile a causa di una potenziale condizione di competizione tra le query SELECT e INSERT/UPDATE. –

1

Una soluzione può essere quella di aggiungere sia l'indice e la validazione del modello.

Così nella migrazione è necessario: add_index: categories_posts, [: category_id,: post_id],: unica => true

E nel modello: validates_uniqueness_of: category_id,: scope => [: category_id ,: post_id] validates_uniqueness_of: post_id,: scope => [: category_id,: post_id]

8

E 'molto difficile raccomandare l'approccio "giusto".

1) L'approccio pragmatico

Usa validatore e non aggiungere indice composito unico. Questo ti dà bei messaggi nell'interfaccia utente e funziona.

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 

È possibile anche aggiungere due indici separati nelle tabelle unirsi per velocizzare le ricerche:

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

Si prega di notare (secondo il libro Rails 3 vie) la validazione non è infallibile, perché di una potenziale condizione di competizione tra le query SELECT e INSERT/UPDATE. Si consiglia di utilizzare un vincolo univoco se si deve assolutamente essere sicuri che non vi siano record duplicati.

2) L'approccio a prova di proiettile

In questo approccio vogliamo mettere un vincolo a livello di database. Quindi vuol dire creare un indice composito:

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

grande vantaggio è un grande integrità del database, svantaggio non è errore molto utile la segnalazione per l'utente. Si prega di notare nella creazione di indice composito, l'ordine delle colonne è importante.

Se mettete le colonne meno selettivi come colonne leader in indice e mettere le colonne più selettivi, alla fine, altre query che hanno condizioni su colonne di indice non-leader possono inoltre usufruire di INDEX SKIP SCAN. Potrebbe essere necessario aggiungere un altro indice per trarne vantaggio, ma questo dipende molto dal database.

3) combinazione di entrambi

Si può leggere su combinazione di entrambi, ma tendo a come il numero uno solo.

+0

Questo dovrebbe essere considerato il migliore in quanto propone il modello e l'integrazione dei dati. –

+0

Non sono d'accordo con la raccomandazione di usare solo il numero uno, ma dato che tutti i pro e contro sono menzionati, questo merita comunque un upvote. – kronn