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