Hack do ActiveRecord UNION

Algumas vezes você precisa mesclar os resultados de duas ou mais consultas no Rails. Uma situação típica para fazer isso é quando você está implementando uma linha do tempo em que busca eventos de você mesmo e também de seus amigos:

class Profile < ActiveRecord::Base
has_many
:events
has_many
:friends
has_many
:friend_events, :through => :friends, :source => :events

def my_timeline
events
+ friend_events
end
end

Isso funcionaria, a menos que, por exemplo, voc√™ compartilhe alguns eventos com seus amigos ou precise classificar os eventos por hora de cria√ß√£o e, em seguida, pegar apenas os √ļltimos. Remover duplicatas ou classificar e limitar os resultados no mundo ruby ‚Äč‚Äčseria terrivelmente lento quando o n√ļmero de eventos aumentasse, ent√£o seria incr√≠vel poder delegar esse trabalho aos nossos bancos de dados otimizados gra√ßas √†s opera√ß√Ķes SQL UNION.

A má notícia é que ActiveRecord ainda não suporta esse tipo de operação, e a boa é que já existem pessoas trabalhando para trazer esse recurso para nossos amados Rails, como você pode ver neste tópico:

https://github.com/rails/arel/pull/118

Enquanto eles encontram uma solução incrível, escrevi este mini-módulo para fingir que minhas modelos podem fazer sindicatos:

module UnionHack
def union(relations, opts={})
query
= 'SELECT '+ (opts[:distinct] ? 'DISTINCT ' : '' ) +'* FROM ((' + relations.map { |r| r.ast.to_sql }.join(') UNION (') + ')) AS t'
query
<< " ORDER BY #{opts[:order]}" if opts[:order]
query
<< " LIMIT #{opts[:limit]}" if opts[:limit]
find_by_sql
(query)
end
end

Adicione-o ao seu projeto, por exemplo, na pasta lib e certifique-se de carreg√°-lo no arquivo application.rb:

# You'll need to add something like that
config
.autoload_paths += %W(#{config.root}/lib)

Em seguida, estenda seu modelo favorito com o módulo:

class Profile < ActiveRecord::Base
extend
UnionHack
...
end

E voc√™ poder√° fazer uni√Ķes com registros exclusivos, classificados por quaisquer campos e limitados em n√ļmero de registros da pr√≥xima maneira:

...
def my_timeline
Profile.union([events, friend_events], :distinct => true, :order => 'created_at DESC', :limit => 20)
end
...

Para sua conveniência, você pode baixar o arquivo do módulo no seguinte Gist:
https://gist.github.com/3662866