【问题标题】:Find or create record through factory_girl association通过 factory_girl 协会查找或创建记录
【发布时间】:2011-10-31 23:53:33
【问题描述】:

我有一个属于组的用户模型。组必须具有唯一的名称属性。用户工厂和组工厂定义为:

Factory.define :user do |f|
  f.association :group, :factory => :group
  # ...
end

Factory.define :group do |f|
  f.name "default"
end

当创建第一个用户时,也会创建一个新组。当我尝试创建第二个用户时,它失败了,因为它想再次创建相同的组。

有没有办法告诉 factory_girl 关联方法首先查找现有记录?

注意:我确实尝试定义一个方法来处理这个问题,但是我不能使用 f.association。我希望能够在这样的 Cucumber 场景中使用它:

Given the following user exists:
  | Email          | Group         |
  | test@email.com | Name: mygroup |

这只有在工厂定义中使用关联时才有效。

【问题讨论】:

    标签: ruby ruby-on-rails-3 cucumber factory-bot


    【解决方案1】:

    您可以将initialize_withfind_or_create 方法一起使用

    FactoryGirl.define do
      factory :group do
        name "name"
        initialize_with { Group.find_or_create_by_name(name)}
      end
    
      factory :user do
        association :group
      end
    end
    

    也可以和id一起使用

    FactoryGirl.define do
      factory :group do
        id     1
        attr_1 "default"
        attr_2 "default"
        ...
        attr_n "default"
        initialize_with { Group.find_or_create_by_id(id)}
      end
    
      factory :user do
        association :group
      end
    end
    

    适用于 Rails 4

    Rails 4 中正确的方法是Group.find_or_create_by(name: name),所以你会使用

    initialize_with { Group.find_or_create_by(name: name) } 
    

    改为。

    【讨论】:

    • 效果很好,谢谢。在 Rails 4 中,首选方式是:Group.find_or_create_by(name: name)
    • Rails 4 中的首选方式实际上是Group.where(name: name).first_or_create
    • 这通常不起作用:Factory Girl 会重置创建的模型。 1.create(:user) 2.Group.first.update_attributes(name: "new name") 3.create(:user).现在Group.first.name == "name" 为真,第 3 步重置第 2 步。在复杂的黄瓜设置中,这很容易发生。对此有何建议?
    • 这很好。但我建议 find_or_initialize_by 所以它可以与 FactoryGirl 的 build 一起使用。
    • 我将扩展 @Kelvin 的回答并提到执行此操作的 safe 方法也是传递原始属性,因此它们不会被初始化如nilModel.where(name: name).first_or_initialize(attributes)
    【解决方案2】:

    我最终使用了在网上找到的各种方法,其中一个是根据 dadyfuzz 在另一个答案中所建议的继​​承工厂。

    我做了以下:

    # in groups.rb factory
    
    def get_group_named(name)
      # get existing group or create new one
      Group.where(:name => name).first || Factory(:group, :name => name)
    end
    
    Factory.define :group do |f|
      f.name "default"
    end
    
    # in users.rb factory
    
    Factory.define :user_in_whatever do |f|
      f.group { |user| get_group_named("whatever") }
    end
    

    【讨论】:

      【解决方案3】:

      您也可以使用 FactoryGirl 策略来实现这一点

      module FactoryGirl
        module Strategy
          class Find
            def association(runner)
              runner.run
            end
      
            def result(evaluation)
              build_class(evaluation).where(get_overrides(evaluation)).first
            end
      
            private
      
            def build_class(evaluation)
              evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@build_class)
            end
      
            def get_overrides(evaluation = nil)
              return @overrides unless @overrides.nil?
              evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@evaluator).instance_variable_get(:@overrides).clone
            end
          end
      
          class FindOrCreate
            def initialize
              @strategy = FactoryGirl.strategy_by_name(:find).new
            end
      
            delegate :association, to: :@strategy
      
            def result(evaluation)
              found_object = @strategy.result(evaluation)
      
              if found_object.nil?
                @strategy = FactoryGirl.strategy_by_name(:create).new
                @strategy.result(evaluation)
              else
                found_object
              end
            end
          end
        end
      
        register_strategy(:find, Strategy::Find)
        register_strategy(:find_or_create, Strategy::FindOrCreate)
      end
      

      您可以使用this gist。 然后执行以下操作

      FactoryGirl.define do
        factory :group do
          name "name"
        end
      
        factory :user do
          association :group, factory: :group, strategy: :find_or_create, name: "name"
        end
      end
      

      不过,这对我有用。

      【讨论】:

      【解决方案4】:

      我遇到了类似的问题并想出了这个解决方案。它按名称查找组,如果找到,它将用户与该组相关联。否则,它会使用该名称创建一个组,然后与之关联。

      factory :user do
        group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
      end
      

      我希望这对某人有用:)

      【讨论】:

        【解决方案5】:

        为了确保 FactoryBot 的 buildcreate 仍然正常运行,我们应该只覆盖 create 的逻辑,方法是:

        factory :user do
          association :group, factory: :group
          # ...
        end
        
        factory :group do
          to_create do |instance|
            instance.id = Group.find_or_create_by(name: instance.name).id
            instance.reload
          end
        
          name { "default" }
        end
        

        这确保build 保持其“构建/初始化对象”的默认行为,并且不执行任何数据库读取或写入,因此它总是很快。只有create 的逻辑被覆盖以获取现有记录(如果存在),而不是总是尝试创建新记录。

        我写了an article 解释了这一点。

        【讨论】:

          【解决方案6】:

          通常我只是做多个工厂定义。一个用于有组的用户,一个用于无组的用户:

          Factory.define :user do |u|
            u.email "email"
            # other attributes
          end
          
          Factory.define :grouped_user, :parent => :user do |u|
            u.association :group
            # this will inherit the attributes of :user
          end
          

          然后,您可以在步骤定义中使用它们来分别创建用户和组,并随意将它们连接在一起。例如,您可以创建一个分组用户和一个单独用户,然后将单独用户加入分组用户团队。

          无论如何,你应该看看pickle gem,它可以让你编写如下步骤:

          Given a user exists with email: "hello@email.com"
          And a group exists with name: "default"
          And the user: "hello@gmail.com" has joined that group
          When somethings happens....
          

          【讨论】:

            【解决方案7】:

            我一直在寻找一种不影响工厂的方法。正如@Hiasinho 指出的那样,创建Strategy 是可行的方法。但是,该解决方案不再适用于我,可能 API 已更改。想出了这个:

            module FactoryBot
              module Strategy
                # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
                # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
                class FindOrCreate
                  def initialize
                    @build_strategy = FactoryBot.strategy_by_name(:build).new
                  end
            
                  delegate :association, to: :@build_strategy
            
                  def result(evaluation)
                    attributes = attributes_shared_with_build_result(evaluation)
                    evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
                  end
            
                  private
            
                  # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
                  # For example, devise's User model is given a `password` and generates an `encrypted_password`
                  # In this case, we shouldn't use `password` in the `where` clause
                  def attributes_shared_with_build_result(evaluation)
                    object_attributes = evaluation.object.attributes
                    evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
                  end
                end
              end
            
              register_strategy(:find_or_create, Strategy::FindOrCreate)
            end
            

            并像这样使用它:

            org = FactoryBot.find_or_create(:organization, name: 'test-org')
            user = FactoryBot.find_or_create(:user, email: 'test@test.com', password: 'test', organization: org)
            

            【讨论】:

              【解决方案8】:

              我正在使用您在问题中描述的 Cucumber 场景:

              Given the following user exists:
                | Email          | Group         |
                | test@email.com | Name: mygroup |
              

              你可以像这样扩展它:

              Given the following user exists:
                | Email          | Group         |
                | test@email.com | Name: mygroup |
                | foo@email.com  | Name: mygroup |
                | bar@email.com  | Name: mygroup |
              

              这将创建 3 个用户组“mygroup”。因为它像这样使用'find_or_create_by' 功能,第一次调用创建组,接下来的两次调用找到已经创建的组。

              【讨论】:

                【解决方案9】:

                另一种方法(适用于任何属性并适用于关联):

                # config/initializers/factory_bot.rb
                #
                # Example use:
                #
                # factory :my_factory do
                #   change_factory_to_find_or_create
                #
                #   some_attr { 7 }
                #   other_attr { "hello" }
                # end
                #
                # FactoryBot.create(:my_factory) # creates
                # FactoryBot.create(:my_factory) # finds
                # FactoryBot.create(:my_factory, other_attr: "new value") # creates
                # FactoryBot.create(:my_factory, other_attr: "new value") # finds
                
                module FactoryBotEnhancements
                  def change_factory_to_find_or_create
                    to_create do |instance|
                      # Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
                      attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
                      instance.attributes = attributes.except('id')
                      instance.id = attributes['id'] # id can't be mass-assigned
                      instance.instance_variable_set('@new_record', false) # marks record as persisted
                    end
                  end
                end
                
                # This makes the module available to all factory definition blocks
                class FactoryBot::DefinitionProxy
                  include FactoryBotEnhancements
                end
                

                唯一需要注意的是,您无法通过 nil 值找到。除此之外,它就像一场梦

                【讨论】:

                  猜你喜欢
                  • 2011-01-04
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-06-05
                  • 1970-01-01
                  • 2020-11-20
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多