在自定义类中使用has_many / belongs_to关联手动分配父ID

时间:2016-02-04 05:28:26

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

我使用自定义类从Feedjirra制作AR实例。我无法让子实例与其父对象相关联。

Show has_many :episodes - Episode belongs_to :show - show_id始终为nil

RSpec记录@show.id@episode.show_id彼此相同。但是,当我在开发中运行导入后运行episode = Episode.first时,该集的show_id设置为nil

@show = Show.new

@show.name = @feed.title
@show.description = @feed.description
...

if @show.save
  puts "@show.id: #{@show.id}"
end

@episodes = []

@feed.entries.each do |item|
    @episodes.push(item)
end

@episodes.each do |item|
    @episode = @show.episodes.new

    @episode.name = item.title
    @episode.description = item.summary
    ...

    if @episode.save
      puts "@episode.show_id: #{@episode.show_id}"
    end
end

我尝试使用@episode = @show.episodes.create@episode = Episode.new@episode.show_id = @show.id。它们都记录了匹配的ID,但实例上的show_id仍为nil。每隔一列都填写正确。

我认为这个问题可能与使用add_foreign_key

有关
class AddShowToEpisodes < ActiveRecord::Migration
  def change
    add_reference :episodes, :show, index: true
    add_foreign_key :episodes, :shows, column: :show_id
  end
end

所以我删除了它并使用了标准foreign_key: true,但它没有效果。

class RemoveShowFromEpisodes < ActiveRecord::Migration
  def change
    remove_column :episodes, :show_id
  end
end

class AddShowBackToEpisodes < ActiveRecord::Migration
  def change
    add_reference :episodes, :show, index: true, foreign_key: true
  end
end

以下是完整代码以防万一。

importers_controller.rb:

class Admin::ImportersController < Admin::ApplicationController
  before_action :set_importer, only: [:show, :edit, :update, :destroy]

  def index
    @importers = policy_scope(Importer)
  end

  def show
  end

  def new
    @importer = Importer.new
    authorize @importer
  end

  def create
    @importer = Importer.new(importer_params)
    authorize @importer

    if @importer.save
      require "subscription_importer"
      SubscriptionImporter.new(@importer)

      flash[:notice] = "Importer added."
      redirect_to admin_importers_path
    else
      flash[:error] = "Importer not added."
      render "new"
    end
  end

  def edit
  end

  def update
  end

  def destroy
  end

  private

  def set_importer
    @importer = Importer.find(params[:id])
    authorize @importer
  end

  def importer_params
    params.require(:importer).permit(:name, :url, :source)
  end
end

subscription_importer.rb:

class SubscriptionImporter
  def initialize(importer)
    @importer = importer
    @feed = Feedjira::Feed.fetch_and_parse @importer.url

    if @importer.source === "iTunes"
      itunes_parser(@importer)
    end
  end

  def itunes_parser(importer)
    @importer = importer

    # Parser
    @feed = Feedjira::Feed.fetch_and_parse @importer.url

    # Show
    @show = Show.new

    @show.name = @feed.title
    @show.description = @feed.description
    @show.logo = @feed.itunes_image
    @show.explicit = explicit_check(@feed.itunes_explicit)
    @show.genre = @feed.itunes_categories
    @show.tags = @feed.itunes_keywords
    @show.url = @feed.url
    @show.language = @feed.language

    if @show.save
      puts "Show import succeeded"
      puts "@show.id: #{@show.id}"
    else
      puts "Show import failed"
    end

    # Episodes
    @episodes = []

    @feed.entries.each do |item|
        @episodes.push(item)
    end

    @episodes.each do |item|
        @episode = @show.episodes.new

        @episode.name = item.title
        @episode.description = item.summary
        @episode.release = item.published
        @episode.image = item.itunes_image
        @episode.explicit = explicit_check(item.itunes_explicit)
        @episode.tags = item.itunes_keywords
        @episode.url = item.enclosure_url
        @episode.duration = item.itunes_duration

        if @episode.save
          puts "Episode import succeeded"
          puts "@episode.show_id: #{@episode.show_id}"
        else
          puts "Episode import failed"
        end
    end
  end

  def explicit_check(string)
    if string == "yes" || "Yes"
      true
    else
      false
    end
  end
end

create_importer_spec.rb:

require "rails_helper"

RSpec.feature "Admins can create importers" do
  let(:user) { FactoryGirl.create(:user, :admin) }

  context "admins" do
    before do
      login_as(user)
      visit "/"
      click_link "Admin"
      click_link "Importers"
      click_link "New Importer"
    end

    scenario "with valid credentials" do
      fill_in "Name", with: "The Stack Exchange Podcast"
      fill_in "Url", with: "https://blog.stackoverflow.com/feed/podcast/" # Needs stubbing
      select "iTunes", from: "Source"
      click_button "Create Importer"

      expect(page).to have_content "Importer added"
      expect(page).to have_content "The Stack Exchange Podcast"
    end

    scenario "with invalid credentials" do
      fill_in "Name", with: ""
      fill_in "Url", with: ""
      click_button "Create Importer"

      expect(page).to have_content "Importer not added"
    end
  end
end

1 个答案:

答案 0 :(得分:0)

我认为episodes类中的SubscriptionImporter功能导致问题......

@episodes = []

@feed.entries.each do |item|
  @episodes.push(item) #-> each "@episodes" is a FeedJirra object
end

@episodes.each do |episode|
   #-> you're now creating an episode in the same call as show, which will either mean that show is not persisted or perhaps some other error 
end

我个人会将SubscriptionImporter功能限制为返回数据。您应该通过相应的模型解析该数据:

#app/controllers/admin/importers_controller.rb
class Admin::ImportersController < Admin::ApplicationController
   def create
      @import = Importer.new import_params 
      if @import.save
         @import.parse_show if @import.itunes?
      end
   end

   private

   def import_params
      params.require(:importer).permit(:name, :url, :source)
   end
end

#app/models/importer.rb
class Importer < ActiveRecord::Base
   def feed
      return false unless itunes?
      origin = Feedjirra::Feed.fetch_and_parse(self.url)
      return {
         name:        origin.title,
         description: origin.description,
         logo:        origin.itunes_image,
         explicit:    explicit_check(origin.itunes_explicit),
         genre:       origin.itunes_categories,
         tags:        origin.itunes_keywords,
         url:         origin.url,
         language:    origin.language,
         entries:     origin.entries
      }
   end

   def parse_show
      Show.create(feed)
   end

   def itunes?
      self.source == "iTunes" #-> true/false 
   end

   private

   def explicit_check
      string == "yes" || "Yes" #-> true/false
   end
end

#app/models/show.rb
class Show < ActiveRecord::Base
   has_many :episodes

   attr_accessor :entries
   after_create :create_episodes #-> might not persist entries

   def create_episodes
      if self.entries.any?
          self.entries.each do |item|
             self.episodes.create({
                name: item.title
                description: item.summary,
                release:     item.published,
                image:       item.itunes_image,
                explicit:    explicit_check?(item.itunes_explicit),
                tags:        item.itunes_keywords,
                url:         item.enclosure_url,
                duration:    item.itunes_duration
             })
          end
      end
   end

   private

   def explicit_check?
      string == "yes" || "Yes"
   end
end

以上内容允许您创建@importer,从中提取feed,然后填充Show&amp; Episode模型包含返回的数据。

虽然解决您的问题,但您需要考虑OOP - 将每个元素作为对象。

<强>更新

如果你想更加客观化,那么就有一个简单的模式:

  1. Importer就是你需要保存的 - 其他一切都应该围绕这个
  2. 发生
  3. Show + Episode 可以为我所知道的所有类/表
  4. 考虑到这一点,您可以执行以下操作:

    #app/controllers/admin/importers_controller.rb
    class Admin::ImportersController < Admin::ApplicationController
       def create
          @import = Importer.new import_params 
          @import.save
       end
    
       private
    
       def import_params
          params.require(:importer).permit(:name, :url, :source)
       end
    end
    
    #app/services/feed.rb
    class Feed
        attr_reader :params, :show, :episode, :origin
        def initialize(params)
          @params = params
        end
    
        def origin
           @origin = Feedjirra::Feed.fetch_and_parse params[:source]
        end
    
        def show
           @show = ShowHelper.new @origin
        end
    
        def episodes
          @show.episodes
        end
    end
    
    #app/services/show_helper.rb
    class ShowHelper 
        attr_reader :origin
        def initialize(origin)
          @origin = origin
        end
    
        def name
          @origin.title
        end
    
        def description
          @origin.summary || @origin.description
        end
    
        def logo
          @origin.itunes_image
        end
    
        def explicit
          %r{^yes$} =~ @origin.itunes_explicit
        end
    
        def genre
          @origin.itunes_categories
        end
    
        def tags
          @origin.itunes_keywords
        end
    
        def url
          @origin.url
        end
    
        def language
          @origin.language
        end
    
        def episodes
          @origin.entries
        end
    end
    
    #app/models/importer.rb
    class Importer < ActiveRecord::Base
       after_create :parse_show, if: "itunes?"
       validates :source, :url, :name, presence: true
    
       def itunes?
          source == "iTunes" 
       end
    
       def feed
          @feed = Feed.new(self)
       end
    
       private
    
       def parse_show
          @show = Show.new(feed.show) if feed && feed.show
          if @show.save && @show.entries.any?
             @show.entries.each do |entry|
                @show.episodes.create ShowHelper.new(entry)
             end
          end
       end
    end