【问题标题】:Find or create a unique associated object in idiomatic ActiveRecord?在惯用的 ActiveRecord 中查找或创建唯一的关联对象?
【发布时间】:2014-08-31 19:02:48
【问题描述】:

如果一个新主题不存在,我如何惯用地创建一个新主题,或者如果该主题存在,我如何创建与会议的关联?

我正在处理一个包含会议和主题的项目。它们都通过一个 ConferenceTopic 对象相互关联。主题的名称是唯一的。

下面是我在会议上创建的名为#find_or_create_topic_with 的自定义方法,它接受名称参数。我这样做是因为当我尝试在会议主题上命名 #find_or_create_by 时,我没有成功为第二个主题命名(下面的 Rails 控制台日志)

The commit for this kludge is on Github.

class Conference < ActiveRecord::Base
  …

  def find_or_create_topic_with(name)
    if topic = Topic.find_by(name: name)
      self.topics.include?(topic) ? topic : self.topics << topic
    else
      self.topics.create(name: name)
    end
  end
end

有关主题的 find_or_create_by 的 Rails 控制台日志

2.1.2 :001 > Conference.last.topics.find_or_create_by(name: "fun")
  Conference Load (1.0ms)  SELECT  "conferences".* FROM "conferences"   ORDER BY "conferences"."id" DESC LIMIT 1
  Topic Load (0.5ms)  SELECT  "topics".* FROM "topics" INNER JOIN "conference_topics" ON "topics"."id" = "conference_topics"."topic_id" WHERE "conference_topics"."conference_id" = $1 AND "topics"."name" = 'fun' LIMIT 1  [["conference_id", 3]]
   (0.1ms)  BEGIN
  Topic Exists (0.4ms)  SELECT  1 AS one FROM "topics"  WHERE "topics"."name" = 'fun' LIMIT 1
  SQL (0.3ms)  INSERT INTO "topics" ("created_at", "name", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["created_at", "2014-08-31 18:57:56.274109"], ["name", "fun"], ["updated_at", "2014-08-31 18:57:56.274109"]]
  SQL (0.5ms)  INSERT INTO "conference_topics" ("conference_id", "created_at", "topic_id", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["conference_id", 3], ["created_at", "2014-08-31 18:57:56.287526"], ["topic_id", 8], ["updated_at", "2014-08-31 18:57:56.287526"]]
   (2.1ms)  COMMIT
 => #<Topic id: 8, name: "fun", created_at: "2014-08-31 18:57:56", updated_at: "2014-08-31 18:57:56">
2.1.2 :002 > Conference.first.topics.find_or_create_by(name: "fun")
  Conference Load (0.6ms)  SELECT  "conferences".* FROM "conferences"   ORDER BY "conferences"."id" ASC LIMIT 1
  Topic Load (0.4ms)  SELECT  "topics".* FROM "topics" INNER JOIN "conference_topics" ON "topics"."id" = "conference_topics"."topic_id" WHERE "conference_topics"."conference_id" = $1 AND "topics"."name" = 'fun' LIMIT 1  [["conference_id", 1]]
   (0.2ms)  BEGIN
  Topic Exists (0.3ms)  SELECT  1 AS one FROM "topics"  WHERE "topics"."name" = 'fun' LIMIT 1
   (0.2ms)  COMMIT
 => #<Topic id: nil, name: "fun", created_at: nil, updated_at: nil>
2.1.2 :003 > Conference.first.topics
  Conference Load (0.5ms)  SELECT  "conferences".* FROM "conferences"   ORDER BY "conferences"."id" ASC LIMIT 1
  Topic Load (0.3ms)  SELECT "topics".* FROM "topics" INNER JOIN "conference_topics" ON "topics"."id" = "conference_topics"."topic_id" WHERE "conference_topics"."conference_id" = $1  [["conference_id", 1]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Topic id: 7, name: "ruby", created_at: "2014-08-31 18:40:40", updated_at: "2014-08-31 18:40:40">]>

以下是查找主题“有趣”的会议的日志

2.1.2 :015 > Topic.find_by(name: "fun").conferences
  Topic Load (0.5ms)  SELECT  "topics".* FROM "topics"  WHERE "topics"."name" = 'fun' LIMIT 1
  Conference Load (0.3ms)  SELECT "conferences".* FROM "conferences" INNER JOIN "conference_topics" ON "conferences"."id" = "conference_topics"."conference_id" WHERE "conference_topics"."topic_id" = $1  [["topic_id", 8]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Conference id: 3, name: "TooLongDidn'tCareConf", location: "Boston", code_of_conduct: false, childcare: false, last_years_attendance: 0, created_at: "2014-08-31 17:47:35", updated_at: "2014-08-31 17:47:35">]>
2.1.2 :016 > Topic.find_by(name: "fun").conferences.count
  Topic Load (0.5ms)  SELECT  "topics".* FROM "topics"  WHERE "topics"."name" = 'fun' LIMIT 1
   (0.3ms)  SELECT COUNT(*) FROM "conferences" INNER JOIN "conference_topics" ON "conferences"."id" = "conference_topics"."conference_id" WHERE "conference_topics"."topic_id" = $1  [["topic_id", 8]]
 => 1

我希望集合代理同时包含第一次和最后一次会议,并且计数为 2。仅使用 #find_or_create_by 我只能将其与首次创建的会议相关联,而不是找到id 为 nil 的主题。

【问题讨论】:

    标签: ruby-on-rails activerecord ruby-on-rails-4 rails-activerecord


    【解决方案1】:

    刚刚阅读了代码,看起来您正在尝试满足三个用例:

    • 主题存在且已加入会议。 => 什么都不做。
    • 主题存在且未加入会议。 => 将主题添加到会议主题中。
    • 主题不存在。 => 创建主题并将其添加到会议主题中。

    我认为您的代码很好。真的。没有什么太粗俗的地方,我会立即想重写它。

    也就是说,我可能会这样写:

    def find_or_create_topic_with(name)
      transaction do
        topic = Topic.where(name: name).first_or_create!
        self.topics.where(topic: topic).first_or_create!
      end
    rescue
      # need to handle uniqueness violations, etc.
    end
    

    一些补充说明:

    • 测试在哪里,兄弟!?
    • 为避免竞争条件产生不良数据,您可能希望为 name 的主题创建一个唯一索引(在没有唯一索引的模型中永远不要有一个 validates_uniqueness 来支持它!)以及另一个会议主题的 topic_id 和 Conference_id。

    如果有任何不妥之处,请随时在 twitter 上联系我。

    【讨论】:

      【解决方案2】:

      它看起来像一个会议 has_many Topics。鉴于此,您应该能够在 .topics 关系上使用 find_or_create_by

      topic = my_conference.topics.find_or_create_by(name: "Name")
      
      # topic will now contain either a newly created Topic that is associated with `my_conference`,
      # or will be the existing Topic with name "Name" from `my_conference.topics`.
      

      【讨论】:

      • 查看上面的控制台日志。它成功创建一个主题,但 find(第二次使用)似乎短路了。也许我需要 find_or_creating 一个 ConferenceTopic,但操作 join 对象似乎更麻烦。
      • 如果主题名称是全球唯一的,那么您不想使用“my_conference.topics”,因为只要没有与给定会议关联的主题名称,它就会创建另一个主题。跨度>
      • 对不起,我一开始没有通读整个控制台,但现在我已经阅读了。但是,我不明白问题出在哪里。在创建具有给定名称的主题后,使用相同名称的 find_or_create 的第二次尝试应该短路并且不应创建任何新内容。那我错过了什么?
      • Michael> 这很有意义。有什么更好的方法来编写它,以便我可以将全球独特的主题与会议联系起来。我应该从 ConferenceTopic 中找到_or_creating 吗?
      • pdobb> 我已更新以更清楚地指示我想要的代码。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-07-28
      • 2014-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多