5

¡No espero que un modelo con NULL como clave foránea pertenezca a algo!Active Record has_many genera sql con clave foránea IS NULL

Tengo la siguiente aplicación de rieles, hormigas de modelado y hormigas (inspiradas en Jozef).

$ rails -v 
Rails 3.2.8 
$ rails new ant_hill 
$ cd ant_hill 

Crea los modelos hormiguero y hormigas. Una hormiga puede pertenecer a un hormiguero y un hormiguero puede tener muchas hormigas.

$ rails generate model AntHill name:string 
$ rails generate model Ant name:string ant_hill_id:integer 
$ vim app/models/ant.rb 
$ cat app/models/ant.rb 
class Ant < ActiveRecord::Base 
    belongs_to :ant_hill 
end 
$ vim app/models/ant_hill.rb 
$ cat app/models/ant_hill.rb 
class AntHill < ActiveRecord::Base 
    has_many :ants 
end 
$ rake db:migrate 
== CreateAntHills: migrating ================================================= 
-- create_table(:ant_hills) 
    -> 0.0013s 
== CreateAntHills: migrated (0.0016s) ======================================== 

== CreateAnts: migrating ===================================================== 
-- create_table(:ants) 
    -> 0.0035s 
== CreateAnts: migrated (0.0037s) ============================================ 

Ejecute el siguiente código en una consola.

$ rails c 
Loading development environment (Rails 3.2.8) 

Crear un par de hormigas, persistieron, que no pertenece a ningún hormiguero.

1.9.2-p290 :001 > Ant.create! name: "August" 
=> #<Ant id: 1, name: "August", ant_hill_id: nil, created_at: "2012-09-27 12:01:06", updated_at: "2012-09-27 12:01:06"> 
1.9.2-p290 :002 > Ant.create! name: "Bertil" 
=> #<Ant id: 2, name: "Bertil", ant_hill_id: nil, created_at: "2012-09-27 12:01:13", updated_at: "2012-09-27 12:01:13"> 

Ahora ejemplifica un hormiguero, pero no lo guardes todavía.

1.9.2-p290 :003 > ant_hill = AntHill.new name: "Storkullen" 
=> #<AntHill id: nil, name: "Storkullen", created_at: nil, updated_at: nil> 

Espero que este hormiguero no tenga ninguna hormiga y no lo hace.

1.9.2-p290 :004 > ant_hill.ants 
=> [] 

Todavía espero que el hormiguero no tenga ninguna hormiga pero ahora tiene dos.

1.9.2-p290 :005 > ant_hill.ants.count 
    (0.1ms) SELECT COUNT(*) FROM "ants" WHERE "ants"."ant_hill_id" IS NULL 
=> 2 

Igual en este caso, nunca debe generar una consulta que contenga "IS NULL" al tratar con claves externas. Quiero decir que "belongs_to NULL" no puede pertenecer a nada, ¿verdad?

1.9.2-p290 :006 > ant_hill.ants.all 
    Ant Load (0.4ms) SELECT "ants".* FROM "ants" WHERE "ants"."ant_hill_id" IS NULL 
=> [#<Ant id: 1, name: "August", ant_hill_id: nil, created_at: "2012-09-27 12:01:06", updated_at: "2012-09-27 12:01:06">, #<Ant id: 2, name: "Bertil", ant_hill_id: nil, created_at: "2012-09-27 12:01:13", updated_at: "2012-09-27 12:01:13">] 

Después de que se conserva, se comporta como se esperaba.

1.9.2-p290 :007 > ant_hill.save! 
=> true 
1.9.2-p290 :008 > ant_hill.ants.count 
    (0.4ms) SELECT COUNT(*) FROM "ants" WHERE "ants"."ant_hill_id" = 1 
=> 0 
1.9.2-p290 :009 > ant_hill.ants.all 
    Ant Load (0.4ms) SELECT "ants".* FROM "ants" WHERE "ants"."ant_hill_id" = 1 
=> [] 

¿Alguna idea? ¿Es este el comportamiento esperado?

Respuesta

1

Si bien parece contradictorio, creo que este comportamiento tiene sentido dado sus ejemplos. Tome ant_hill.ants.count por ejemplo. Count es un método de consulta de ActiveRecord que llega a la base de datos, y esencialmente le estás pidiendo a ActiveRecord que te dé todas las hormigas que no pertenecen a un hormiguero. Rails simplemente le permite hacer algo que no debería poder hacer y no quejarse de ello. ¿Debería esto generar una excepción? Posiblemente.

Si realmente quieres saber cuántas hormigas pertenecen a este objeto ant_hill, debes usar el tamaño. Consulta el objeto cuando no persiste o cuando la asociación ya está cargada, y consulta la base de datos de otra manera.

ant_hill.ants.size 

Una forma de evitar esta rareza es hacer que ant_hill_id sea un campo obligatorio al validar su presencia.

TL; DR Evite el uso de la interfaz de consulta ActiveRecord si el objeto principal no se conserva en la base de datos.

+0

No puedo hacer que se requiera ant_hill_id en este caso porque mi aplicación permite hormigas que no pertenecen a ningún hormiguero. – ludde

+0

usando '.size' debería funcionar en su caso – PinnyM

+1

Para ser claros, ya estoy trabajando en torno a este problema en mi aplicación y reconozco que hay muchas maneras de evitar esto. Simplemente no creo que sea el comportamiento esperado.Cuando algo tiene una clave externa NULL, eso significa que no pertenece a nada. Supongo que podría argumentar que un hormiguero no salvado no es nada desde la perspectiva de la base de datos, pero realmente no. – ludde

Cuestiones relacionadas