sql - ActiveRecord Arel O condición
ruby-on-rails ruby (10)
¿Qué pasa con este enfoque ?: http://guides.rubyonrails.org/active_record_querying.html#hash-conditions (y verifique 2.3.3)
admins_or_authors = User.where(:kind => [:admin, :author])
¿Cómo se pueden combinar 2 condiciones diferentes usando OR lógico en lugar de Y?
NOTA: se generan 2 condiciones como alcances de raíles y no se puede cambiar fácilmente a algo así como where("x or y")
directamente.
Ejemplo simple:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
Es fácil aplicar AND condición (que para este caso particular no tiene sentido):
(admins.merge authors).to_sql
#=> select ... from ... where kind = ''admin'' AND kind = ''author''
Pero, ¿cómo se puede producir la siguiente consulta que tiene 2 relaciones Arel diferentes ya disponibles?
#=> select ... from ... where kind = ''admin'' OR kind = ''author''
Parece ( según Arel readme ):
El operador OR aún no es compatible
Pero espero que no se aplique aquí y espero escribir algo como:
(admins.or authors).to_sql
A partir de Rails 5 tenemos ActiveRecord::Relation#or
, lo que le permite hacer esto:
User.where(kind: :author).or(User.where(kind: :admin))
... que se traduce al SQL que esperarías:
>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = ''author'' OR "users"."kind" = ''admin'')
De la página arel real :
El operador OR funciona así:
users.where(users[:name].eq(''bob'').or(users[:age].lt(25)))
Lamentablemente, no es compatible de forma nativa, por lo que debemos hackear aquí.
Y el truco se ve así, que es bastante ineficiente SQL (espero que los DBA no lo estén viendo :-)):
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
both = User.where("users.id in (#{admins.select(:id)}) OR users.id in (#{authors.select(:id)})")
both.to_sql # => where users.id in (select id from...) OR users.id in (select id from)
Esto genera subselets.
Y un truco un poco mejor (desde la perspectiva de SQL) se ve así:
admins_sql = admins.arel.where_sql.sub(/^WHERE/i,'''')
authors_sql = authors.arel.where_sql.sub(/^WHERE/i,'''')
both = User.where("(#{admins_sql}) OR (#{authors_sql})")
both.to_sql # => where <admins where conditions> OR <authors where conditions>
Esto genera una condición OR apropiada, pero obviamente solo tiene en cuenta la parte DONDE de los ámbitos.
Elegí el primero hasta que vea cómo funciona.
En cualquier caso, debe ser muy cuidadoso con eso y ver el SQL generado.
Las consultas de ActiveRecord son objetos ActiveRecord::Relation
(que enloquecentemente no admiten or
), no objetos Arel (que sí lo hacen).
[ ACTUALIZACIÓN : a partir de Rails 5, "o" es compatible con ActiveRecord::Relation
; ver https://.com/a/33248299/190135 ]
Pero afortunadamente, su método where acepta objetos de consulta de ARel. Entonces, si el User < ActiveRecord::Base
...
users = User.arel_table
query = User.where(users[:kind].eq(''admin'').or(users[:kind].eq(''author'')))
query.to_sql
ahora muestra la tranquilidad:
SELECT "users".* FROM "users" WHERE (("users"."kind" = ''admin'' OR "users"."kind" = ''author''))
Para mayor claridad, puedes extraer algunas variables temporales de consulta parcial:
users = User.arel_table
admin = users[:kind].eq(''admin'')
author = users[:kind].eq(''author'')
query = User.where(admin.or(author))
Y, naturalmente, una vez que tenga la consulta, puede usar query.all
para ejecutar la llamada a la base de datos real.
Llego un poco tarde a la fiesta, pero esta es la mejor sugerencia que pude hacer:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)
User.where(admins.or(authors)).to_sql
# => "SELECT /"users/".* FROM /"users/" WHERE ((/"users/"./"kind/" = ''admin'' OR /"users/"./"kind/" = ''author''))"
Me he topado con el mismo problema buscando una alternativa activerecord a #any_of de #any_of
.
La respuesta de @jswanner es buena, pero solo funcionará si los parámetros where son Hash:
> User.where( email: ''foo'', first_name: ''bar'' ).where_values.reduce( :and ).method( :or )
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>
> User.where( "email = ''foo'' and first_name = ''bar''" ).where_values.reduce( :and ).method( :or )
NameError: undefined method `or'' for class `String''
Para poder usar cadenas y hashes, puede usar esto:
q1 = User.where( "email = ''foo''" )
q2 = User.where( email: ''bar'' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )
De hecho, eso es feo, y no quiere usarlo a diario. Aquí hay algunos #any_of
implementación que he hecho: https://gist.github.com/oelmekki/5396826
Dejo eso:
> q1 = User.where( email: ''foo1'' ); true
=> true
> q2 = User.where( "email = ''bar1''" ); true
=> true
> User.any_of( q1, q2, { email: ''foo2'' }, "email = ''bar2''" )
User Load (1.2ms) SELECT "users".* FROM "users" WHERE (((("users"."email" = ''foo1'' OR (email = ''bar1'')) OR "users"."email" = ''foo2'') OR (email = ''bar2'')))
Editar: desde entonces, he publicado una joya para ayudar a crear consultas OR .
Para extender la respuesta de jswanner (que en realidad es la mejor solución y me ayudó) para buscar en Google:
puedes aplicar un alcance como este
scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
with_glob = where(owner_id: nil).where_values.reduce(:and)
where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}
User.with_owner_ids_or_global(Developer, 1, 2)
# => ...WHERE `users`.`owner_type` = ''Developer'' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
Simplemente haga un alcance para su condición OR:
scope :author_or_admin, where([''kind = ? OR kind = ?'', ''Author'', ''Admin''])
Usar SmartTuple se verá más o menos así:
tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)
O
User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)
Puede pensar que soy parcial, pero aún considero que las operaciones tradicionales de estructura de datos son mucho más claras y convenientes que el encadenamiento de métodos en este caso particular.