Rails find_or_create_by mais de um atributo?

202

Existe um atributo dinâmico útil no registro ativo chamado find_or_create_by:

Model.find_or_create_by_<attribute>(:<attribute> => "")

Mas e se eu precisar localizar ou criar mais de um atributo?

Digamos que eu tenha um modelo para lidar com um relacionamento M: M entre Grupo e Membro chamado GroupMember. Eu poderia ter muitas instâncias em que member_id = 4, mas nunca mais quero uma instância em que member_id = 4 e group_id = 7. Estou tentando descobrir se é possível fazer algo assim:

GroupMember.find_or_create(:member_id => 4, :group_id => 7)

Sei que pode haver maneiras melhores de lidar com isso, mas gosto da conveniência da idéia de find_or_create.

tybro0103
fonte

Respostas:

464

Vários atributos podem ser conectados com um and:

GroupMember.find_or_create_by_member_id_and_group_id(4, 7)

(use find_or_initialize_byse você não quiser salvar o registro imediatamente)

Edit: O método acima está obsoleto no Rails 4. A nova maneira de fazer isso será:

GroupMember.where(:member_id => 4, :group_id => 7).first_or_create

e

GroupMember.where(:member_id => 4, :group_id => 7).first_or_initialize

Edit 2: Nem todos estes foram fatorados fora dos trilhos, apenas os específicos do atributo.

https://github.com/rails/rails/blob/4-2-stable/guides/source/active_record_querying.md

Exemplo

GroupMember.find_or_create_by_member_id_and_group_id(4, 7)

passou a ser

GroupMember.find_or_create_by(member_id: 4, group_id: 7)
x1a4
fonte
Também recomendamos github.com/seamusabshere/upsert - o primeiro argumento é um hash de atributos que é usado para encontrar ou criar um registro
Seamus Abshere
1
É útil notar que initialize chama a criar () em vez de novo () que pode ser chamado em caso de first_or_create
Sumit Bisht
Alguém pode compartilhar um exemplo essencial desse uso? Não estou familiarizado com sua localização e chamada.
6 Dan Dan
Reverti a remoção do código do Rails 3, pois muitas pessoas ainda não estão usando o Rails 4.
Kyle Heironimus
Apenas para adicionar: as funções find_or_create_by _ * () foram descontinuadas no Rails 4, no entanto, find_or_create_by () NÃO é. Não há problema em usar find_or_create_by (). Verifique o commit no qual foi adicionado github.com/rails/rails/commit/… e a documentação oficial do Rails 4.2 para o uso: guias.rubyonrails.org/v4.2.0/…
CodeExpress
33

Para qualquer pessoa que se depara com esse encadeamento, mas precisa localizar ou criar um objeto com atributos que possam mudar dependendo das circunstâncias, adicione o seguinte método ao seu modelo:

# Return the first object which matches the attributes hash
# - or -
# Create new object with the given attributes
#
def self.find_or_create(attributes)
  Model.where(attributes).first || Model.create(attributes)
end

Dica de otimização: independentemente de qual solução você escolher, considere adicionar índices para os atributos que você está consultando com mais frequência.

Marco
fonte
8
Uma coisa a observar é que isso não manipula atributos que não podem ser atribuídos em massa, enquanto o find_or_create_byfará.
x1a4
1
Marco, tente Model.where(attributes).instance_eval{|q| q.first || q.create}.
Hirson12
3
find_or_createtambém tem a vantagem de usar uma transação, em que where….first || createintroduz uma condição de corrida
mrm
Eu diria def self.find_or_create(attributes) self.where(attributes).first || self.create(attributes) endpara que você não precisa repetirModel
Augustin Riedinger
@AugustinRiedinger Você também não precisa selfdo corpo do método (já que deseja remover as palavras!).
David Tuite
31

No Rails 4, você poderia fazer:

GroupMember.find_or_create_by(member_id: 4, group_id: 7)

E o uso whereé diferente:

GroupMember.where(member_id: 4, group_id: 7).first_or_create

Isso vai chamar createem GroupMember.where(member_id: 4, group_id: 7):

GroupMember.where(member_id: 4, group_id: 7).create

Pelo contrário, o find_or_create_by(member_id: 4, group_id: 7)chamará createem GroupMember:

GroupMember.create(member_id: 4, group_id: 7)

Por favor, veja este commit relevante nos trilhos / trilhos.

Juanito Fatas
fonte
16

Ao passar um bloco para find_or_create, você pode passar parâmetros adicionais que serão adicionados ao objeto se ele for criado novo. Isso é útil se você estiver validando a presença de um campo que não está pesquisando.

Assumindo:

class GroupMember < ActiveRecord::Base
    validates_presence_of :name
end

então

GroupMember.where(:member_id => 4, :group_id => 7).first_or_create { |gm| gm.name = "John Doe" }

criará um novo GroupMember com o nome "John Doe" se não encontrar um com member_id 4 and group_id 7

Daniel Murphy
fonte
4

Você pode fazer:

User.find_or_create_by(first_name: 'Penélope', last_name: 'Lopez')
User.where(first_name: 'Penélope', last_name: 'Lopez').first_or_create

Ou apenas para inicializar:

User.find_or_initialize_by(first_name: 'Penélope', last_name: 'Lopez')
User.where(first_name: 'Penélope', last_name: 'Lopez').first_or_initialize
Dorian
fonte