Find or create a unique related object in idiomatic ActiveRecord?

How do I idiomatically create a new topic if it doesn't exist, or create a link to conferences if a topic exists?

I am working on a project that has conferences and topics. They are both linked to each other through the ConferenceTopic object. Topic titles are unique.

Below is a custom method created in conferences called #find_or_create_topic_with

that takes a name argument. I did this because when I tried to use the #find_or_create_by

name in the conference thread, I couldn't make it second for the log (Rails Console log below)

The end 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

      

Rails console for find_or_create_by by topic

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">]>

      

Below is the log to search for conferences for topic "fun"

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

      

I would like the collection proxy to contain both the first and last conferences, and the counter is 2. With just the help #find_or_create_by

I can only associate it with the conference for which it was first created, not for subsequent conferences which are the topic with the identifier nil.

+3


source to share


2 answers


Just by reading the code, it looks like you are trying to satisfy three use cases:

  • The topic exists and is already connected to the conference. => Don't do anything.
  • The topic exists and is not joining the conference. => Add topic to conference topics.
  • The topic does not exist. => Create a topic and add it to conference topics.

I think your code is fine. Indeed. There is nothing crude about it, where I would immediately want to rewrite it.

However, I would probably write it like this:



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

Some additional notes:

  • Where are the tests, bro !?
  • To avoid race conditions creating bad data, you probably need a unique topic index for the name (you will never have validates_uniqueness in a model without a unique index to back it up!) And another on the conference topics for topic_id and conference_id.

Feel free to push me to twitter or whatever if that doesn't make sense.

+1


source


Sounds like the has_many Topics conference. With that in mind, you should simply use find_or_create_by

in relation .topics

:



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`.

      

0


source







All Articles