Buenas. A ver si alguien sabe algo.
pongamos este caso:
class User < ActiveRecord::Base
has_many :users_friends
has_many :friends, :through => :users_friends, :extend =>
UsersProperties
end
class UsersFriend < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
end
class UsersFriendSweeper < ActionController::Caching::Sweeper
observe UsersFriend
def after_save(user_relatioship)
expire_fragment("barra_de_amigos_#{user_relatioship.user.id}")
end
end
EOF
El sweeper funciona normalmente, excepto en casos como:
User.find_by_username("guillermo").friends <<
User.find_by_username("cientifico")
Donde el swepper no es llamado. Me parece lógico (salvo que esté
metiendo la pata), que al no intervenir el model UsersFriend
(internamente tirará de joins de la base de datos), directamente el
Observer no se ejecute.
Lo que pregunto es ¿Qué soluciones le véis a este problema? Cuando
digo soluciones, me refiero a soluciones algo más limpias que poner el
código en la acción, o no usar las relaciones de rails y crear
amistades a la vieja usanda UsersFriend.create(:user =>
User.find_by_username("guillermo"), :friend=>
User.find_by_username("cientifico")). Estoy pensando en alguna forma
de hacer que AR invoque el swepper a mano... pero no se me ocurre.
Muchas gracias.
on 13.08.2008 18:30
on 13.08.2008 19:36
Guillermo, sin haber provado el cdigo, y findome de lo que dices, creo que esto deberia ser considerado un BUG de ActiveRecord, ya que se est haciendo un insert en la tabla al crear la relacin con el operador << Como observacin, en el sweeper no faltaria un after_destroy? Se supone que si tu y el cientifico os enfadais y dejais de ser amigos deberia reflejarse en la barra de amigos. ;) Has provado utilizando after_create en lugar de after_save? Salutaciones, -- Isaac Feliu
on 14.08.2008 00:26
2008/8/13 Guillermo <guillermo@cientifico.net>: > belongs_to :user > EOF > > -- > Guillermo Álvarez Acabo de hacer una prueba (Rails 2.1, pero he visto el código de Rails 2.0 y 1.2.6 y no era muy diferente) y ha funcionado correctamente. Mí código es el siguiente (los "cambios" los explico en comentarios inline): --- class User < ActiveRecord::Base # Pongo el class_name pq llamé a la clase en plural y no me apeticia cambiarla, # no debería ser importante has_many :users_friends, :class_name => 'UsersFriends' # Falta tu extend que no debería influir has_many :friends, :through => :users_friends end # Creo que esta es igual excepto el nombre class UsersFriends < ActiveRecord::Base belongs_to :user belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id' end # Esta también es igual pero en vez del expire hago un log para verlo. class UsersFriendsSweeper < ActionController::Caching::Sweeper observe UsersFriends def after_save(user_friend) user_friend.logger.info('¡Ay omá que rica!') end end --- Para disparar el sweeper en vez de montar una aplicación entera he montado un script en lib con una técnica que discutimos en la lista hace una semana: --- # require 'ruby-debug' require 'action_controller/test_process' ActiveRecord::Base.observers = [UsersFriendsSweeper] ActiveRecord::Base.instantiate_observers UsersFriendsSweeper.instance.controller = ActionController::Base.new tr = ActionController::TestRequest.new UsersFriendsSweeper.instance.controller.request = tr UsersFriendsSweeper.instance.controller.instance_eval do @url = ActionController::UrlRewriter.new(tr, {}) end u1 = User.create :name => 'usuario' + rand(1000).to_s u2 = User.create :name => 'usuario' + rand(1000).to_s # debugger u1.friends << u2 --- Como ves es "similar" al ejemplo que pones tú, y en el log aparece: --- User Create (0.000907) INSERT INTO "users" ("name", "updated_at", "created_at") VALUES('usuario518', '2008-08-13 22:23:22', '2008-08-13 22:23:22') User Create (0.000451) INSERT INTO "users" ("name", "updated_at", "created_at") VALUES('usuario417', '2008-08-13 22:23:22', '2008-08-13 22:23:22') UsersFriends Create (0.000560) INSERT INTO "users_friends" ("updated_at", "user_id", "friend_id", "created_at") VALUES('2008-08-13 22:23:22', 13, 14, '2008-08-13 22:23:22') ¡Ay omá que rica! --- Es decir, el sweeper sí se dispara (y se crean todos los registros correspondientes a los modelos). Comprueba si en tu log aparece la línea "UsersFriends Create", y comprueba que tu sweeper se carga en environement.rb o similar (aunque si dices que "funciona normalmente" en otras situaciones debería estar cargado correctamente). Suerte.
on 14.08.2008 01:20
2008/8/14 Daniel Rodriguez Troitiño <notzcoolx@yahoo.es>: > Es decir, el sweeper sí se dispara (y se crean todos los registros > correspondientes a los modelos). > Lo acabo de comprobar, (yo si me hice la aplicación entera). > Comprueba si en tu log aparece la línea "UsersFriends Create", y > comprueba que tu sweeper se carga en environement.rb o similar (aunque > si dices que "funciona normalmente" en otras situaciones debería estar > cargado correctamente). Solo me queda que en 1.2.6 no funcione, o lo que cada vez veo más factible, que la haya cagado en algún punto. Mañana lo comento. Muchas gracias por las respuestas, y si resulta que metí la pata: Mis disculpas.
on 14.08.2008 12:57
Vale. Se ha juntado algún fallo mio, con lo que creo que es un bug de
rails
Ya encuentro por que falla.
El método << que es el que se utiliza para añadir a los amigos, sí
llama al observer.
El método delete, NO llama al observer.
Más claro.
Pongamos acción destroy, y si ponemos
@users_friend = UsersFriend.find(params[:id])
@users_friend.user.friends.delete(@users_friend.friend)
No se llama al observer
Sin embargo si ponemos
@users_friend.destroy
Si se llama.
He creado un ticket[1], ya que si cosas como:
@users_friend.user.friends << (@users_friend.friend)
si invocan al sweeper,
@users_friend.user.friends.delete(@users_friend.friend)
creo que también debería invocarlo.
¿Qué opinan?
[1]
http://rails.lighthouseapp.com/projects/8994/tickets/826-delete-in-a-relation-doesn-t-call-sweeper#ticket-826-1
on 14.08.2008 14:02
2008/8/14 Guillermo <guillermo@cientifico.net>: > No se llama al observer > creo que también debería invocarlo. > > ¿Qué opinan? > > > [1] http://rails.lighthouseapp.com/projects/8994/tickets/826-delete-in-a-relation-doesn-t-call-sweeper#ticket-826-1 > Parece ser que cuando haces delete de una asociación has_many :through se hace delete de los registros, y no destroy, por lo que nunca se dispararán los callbacks. Una solución alternativa parece ser utilizar callbacks :after_remove de la asociación (se pasan como opción a la asociación), pero no he conseguido hacer que funcionen desde un Observer/Sweeper (ni desde User ni desde UsersFriend). Según la documentación un Observer puede utilizar los callbacks del módulo Callback, por desgracia :after_remove no está en él (y por lo poco que he encontrado en Google estos callbacks no funcionaban en has_many:through hasta la 2.1). En Rails-doc (según Google) hay alguna nota sobre estos callbacks, pero ahora está en mantenimiento y no he podido leerla, quizá aclare algo. Suerte.
on 14.08.2008 15:04
Comprendo que un user.friends.delete(friend) no llame a los callbacks. Hay que estar atento, por que a nivel informático es lógico (por lo menos para mi) que no lo haga, sin embargo a nivel conceptual, si defino unos callbacks para antes de que una relación se destruya, esos callbacks, considero que se debería ejecutar. Soy consciente de que user.friends << friend no es el mísmo método, pero veo poco coherente este comportamiento (En un caso sí y en otro no) Si tu tienes una abstracción de la base de datos, esta debería de ser uniforme independientemente de como accedas a los datos que esta contiene. Es esa la razón por lo que considero que es un bug. Creo que no debería tener cuidado de que manera borro una/unas filas de la base de datos. Y hasta que esto se solucionase, creo que debería de desactivarse el uso de los callbacks (y notificarse en la documentación) hasta que esté soportado en todos los métodos. Soy programador y hago un software en función de la api. No debería de tener que pensar o revisar el intríngulis del asunto. Si este fin de semana saco tiempo, haré la prueba con datamapper, para ver si le pasa lo mismo. Un Saludo.
on 14.08.2008 16:38
2008/8/14 Guillermo <guillermo@cientifico.net>: > Si tu tienes una abstracción de la base de datos, esta debería de ser > revisar el intríngulis del asunto. > > Si este fin de semana saco tiempo, haré la prueba con datamapper, para > ver si le pasa lo mismo. > > Un Saludo. > > > -- > Guillermo Álvarez Sí, los callbacks en Rails a veces lian más las cosas de las que las arreglan. Sólo hay que mirar que los counter de las asociaciones son incrementados o no dependiendo del método utilizado. Y en realidad todo es un poco culpa de no querer confiar en las bases de datos, porque los trigger de las bases de datos se ejecutarían siempre, ya utilizasemos destroy o delete (sí, ya se que escribir todo en Ruby es más cómodo ;) ). He encontrado una (rebuscada) forma para que funcione... al menos en Rails 2.1 y Ruby 1.8.6: --- class User < ActiveRecord::Base has_many :users_friends, :class_name => 'UsersFriends' has_many :friends, :through => :users_friends, :before_remove => :fire_before_remove_of_users_friends private def fire_before_remove_of_users_friends(friend) uf = self.users_friends.find_by_friend_id(friend.id) uf.class.changed uf.class.notify_observers(:before_remove, uf) end end --- Utilizo before_remove y no after_remove porque en el momento de disparse este último ya no existe el UsersFriends. Supongo que tu callback únicamente borra la cache y no realiza más acciones, pero debes tener en cuenta que puede que la asociación no sea destruida (algo que no importa si borras la cache, ya que al regenerarla se volverá a crear correctamente). Luego es cuestion de tener un before_remove en tu Sweeper y todo listo. Espero que te sirva. Suerte.
on 14.08.2008 20:47
Te has mirado la documentación de Rails. Yo creo que lo dice bien claro: delete(id) [1] "Delete an object (or multiple objects) where the id given matches the primary_key. A SQL DELETE command is executed on the database which means that no callbacks are fired off running this. This is an efficient method of deleting records that don't need cleaning up after or other actions to be taken. Objects are not instantiated with this method." [1] http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001306 Hace un SQL DELETE sin instanciar los objetos. Es una manera eficiente de borrar por ejemplo unos cuantos centenares de registros.
on 14.08.2008 21:45
On Thu, Aug 14, 2008 at 20:46, Francesc Esplugas <francesc.esplugas@gmail.com> wrote: > > [1] http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M001306 > > Hace un SQL DELETE sin instanciar los objetos. Es una manera eficiente > de borrar por ejemplo unos cuantos centenares de registros. Sí, en ese punto de la documentación está muy claro, pero en la documentación de has_many solo pone lo siguiente: collection.delete(object, ...) - Removes one or more objects from the collection by setting their foreign keys to NULL. This will also destroy the objects if they're declared as belongs_to and dependent on this model. En el primer caso utiliza "remove" que no deja claro si es un "delete" o un "destroy", y luego comenta algo sobre "destroy" y "belongs_to"... pero en la documentación de belongs_to dice: :dependent - If set to :destroy, the associated object is destroyed when this object is. If set to :delete, the associated object is deleted without calling its destroy method. This option should not be specified when belongs_to is used in conjunction with a has_many relationship on another class because of the potential to leave orphaned records behind. Así que no lo entiendo, en un lado te dicen que declares en el belongs_to que eres dependent del otro modelo, y luego te dicen que no deberías en relaciones has_many. Habría que hacer las pruebas pertinentes, pero quizá con un par de dependent en los belongs_to del join model consiga dispararse el callback (al ser destruido el objeto y no eliminado). Pero la queda de Guillermo (y la mía) es que en ese punto Rails es contraproductivo: tu esperas que si eliminas un registro sus callbacks after_destroy sean invocados. Digamos que si utilizas ActiveRecord::Base#delete **a posta** sabes que los callbacks no van a ser invocados, pero si utilizas un método como collection.delete Rails no te da la oportunidad de "activar" o "desactivar" los callbacks.
on 17.08.2008 04:02
Esto no esta muy bien documentado, le echare un repaso.
En las has_many normales #delete invoca al destroy del hijo si la
opcion :dependent es :destroy. En las has_many :trough no es que haya
un bug, es que no esta hecho:
# TODO - add dependent option support
def delete_records(records)
klass = @reflection.through_reflection.klass
records.each do |associate|
klass.delete_all(construct_join_attributes(associate))
end
end
Ahi procede un parchecito veraniego :-).
on 17.08.2008 11:45
2008/8/17 Xavier Noria <fxn@hashref.com>: > klass.delete_all(construct_join_attributes(associate)) > end > end > > Ahi procede un parchecito veraniego :-). No entiendo una cosa sobre la documentación y el código. Me parece que la opción dependent está funcionando en dos situaciones algo diferentes y no se si son compatibles o al menos una de ellas debería estar reflejada en la documentación (ahora no lo está). Me explico: según la documentación de has_many la opción :dependent sirve para: :dependent - If set to :destroy all the associated objects are destroyed alongside this object by calling their destroy method. If set to :delete_all all associated objects are deleted without calling their destroy method. If set to :nullify all associated objects' foreign keys are set to NULL without calling their save callbacks. Warning: This option is ignored when also using the :through option. En la primera frase dice que si utilizas :destroy los objetos asociados se destruyen cuando el objeto se destruye, pero en la segunda y tercera frases me dan a entender que que también la opción se utiliza para decidir que sucede cuando se eliminan objetos de la asociación. Obviamente la última frase se refiere al TODO que has comentado. Supongo que ese TODO está ahí porque has_many :through evoluciona desde habtm, donde no existen tales problemas al no existir un modelo join. En mi opinión, con un modelo join debería hacerse caso de la opción :dependent para eliminar o destruir los registros del modelo join (y de esta forma se puedan disparar los callbacks que necesitaba Guillermo). No me parece un parche complicado (más o menos es utilizar el delete_records de has_many, ignorando la opción por defecto de :nullify). Y a la vez podría modificarse la documentación para explicar exactamente el funcionamiento de la opción :dependent, y aclarar el funcionamiento por defecto cuando se utiliza la opción :through. Cuando envies el parche no olvides mandar el enlace a la lista para que lo evaluemos y le demos nuestros +1 ;). Suerte.
on 18.08.2008 13:43
2008/8/17 Daniel Rodriguez Troitiño <notzcoolx@yahoo.es>: > No me parece un parche complicado (más o menos es utilizar el > delete_records de has_many, ignorando la opción por defecto de > :nullify). Y a la vez podría modificarse la documentación para > explicar exactamente el funcionamiento de la opción :dependent, y > aclarar el funcionamiento por defecto cuando se utiliza la opción > :through. El problema que hay es más filosófico, y dependerá de la filosofía que lleven ahora los cores. El no instanciar objetos para realizar un simple sql da un rendimiento más o menos óptimo. Esto por ejemplo nos permite borrar más o menos rápido sin consumo de cpu/memoria, todas sus relaciones (posts, mensajes, acciones, etc...). Para que se llamen a los callbacks, habría que instanciar cada objeto y hacer un destroy. Esto multiplicaría el número de deletes de uno por relación a el número total de elementos. Instanciar cada fila para hacer un destroy puede llegar a ser muy costoso. La verdad, yo no me decanto por cual de las dos opciones es mejor. Pero si no puedes construir una api uniforme... la solución creo que es simplificar y no permitirlo nunca, o remarcar mucho más de lo que está ahora en que casos. Todavía no he mirado como lo hace datamapper.
on 18.08.2008 14:10
2008/8/18 Guillermo <guillermo@cientifico.net>: > El no instanciar objetos para realizar un simple sql da un rendimiento > es simplificar y no permitirlo nunca, o remarcar mucho más de lo que > está ahora en que casos. > Sí, la sobrecarga de un destroy sobre cada objeto la entiendo (frente a un simple "DELETE FROM table"), pero las relaciones has_many proporcionan la opción de pedir *explicitamente* que se utilicen destroys en vez de deletes (bueno, en realidad en las relaciones has_many por defecto simplemente ponen a null el foreign key, sin disparar callbacks, por cierto). Obviamente los desarrolladores han escogido la opción menos cara (actualizar, frente a borrar, frente a destruir), pero al menos han proporcionado la opción de que el usuario elija una de las otras. En el caso de has_many :through la opción de poner a null el foreign key no tiene mucho sentido (se quedarían modelos join por ahí colgando), por lo que la opción de eliminar es la que tiene más sentido y es menos cara. El problema es que los desarrolladores han dejado como TODO proporcionar al usuario la opción de decidir que política se seguirá, cuando, imho, no es tan complicado añadirlo y documentarlo. Espero que el parche sea aceptado, porque recurrir a las triquiñuelas que se han comentado en el hilo me parece... poco profesional.
on 18.08.2008 15:02
2008/8/18 Daniel Rodriguez Troitiño <notzcoolx@yahoo.es>: > El problema es que los desarrolladores han > dejado como TODO proporcionar al usuario la opción de decidir que > política se seguirá, cuando, imho, no es tan complicado añadirlo y > documentarlo. Añadirlo no es complicado. Añadirlo y que el rendimiento sea comparable al actual, creo que si. > > Espero que el parche sea aceptado, porque recurrir a las triquiñuelas > que se han comentado en el hilo me parece... poco profesional. No se si lo comenté más arriba, pero al final decidí, incluso instaurármelo como costumbre, el hacerlo sin magia. UserFriend.create(:user_id => current_user.id, :friend_id => params[:id]) y UserFriend.find(:first, :conditions => "...").destroy Así me aseguro que siempre se llaman los callbacks. Un Saludo.
on 18.08.2008 16:57
2008/8/18 Guillermo <guillermo@cientifico.net>: > 2008/8/18 Daniel Rodriguez Troitiño <notzcoolx@yahoo.es>: >> El problema es que los desarrolladores han >> dejado como TODO proporcionar al usuario la opción de decidir que >> política se seguirá, cuando, imho, no es tan complicado añadirlo y >> documentarlo. > > Añadirlo no es complicado. Añadirlo y que el rendimiento sea > comparable al actual, creo que si. El rendimiento sería el mismo para la opción por defecto. El rendimiento con :destroy sería el mismo que la misma opción de has_many (no :through) con :destroy. Bueno, vale, hay que añadirle un if, pero, a pesar que creo que Ruby tiene algún que otro problema de eficiencia, yo no me preocuparía por un if...else. > Así me aseguro que siempre se llaman los callbacks. > Acuerdate de documentarlo y explicarlo, para que el que venga después no se rasque la cabeza pensando porqué has hecho eso, o peor aún lo cambié sin pensar :D.