Построить сложные рельсы SQL - PullRequest
5 голосов
/ 21 октября 2011

Я использую postgreSQL. Нужна область для получения недавних шуток с punchlines_count. Я уже достигаю этого с моей областью. Что мне нужно знать, так это то, что в моем счетчике punchline не должно быть punchlines, у которого есть warn_level> 1. У модели предупреждения есть punchline_id и weight, weight == warn_level. Пожалуйста, помогите мне построить этот запрос. Пояснения: предупреждение «Изюминка» может быть с весом 1 или 2, или может иметь 2 предупреждения с весом 1 каждый. Если warn_level> 1, я не должен считать это в своей области видимости. Спасибо!

Мои модели.

class Joke < ActiveRecord::Base
  COLUMNS = self.column_names.map{|c| "jokes.#{c}" }.join(', ') 
  has_many :punchlines, :dependent => :destroy

  scope :recent, :order => 'jokes.created_at DESC'
  scope :recent_jokes_with_punchline_counter, lambda { |limit|
                        select("#{Joke::COLUMNS}, COUNT(punchlines.id) as punchlines_count").
                                             joins(:punchlines).
                                             group(Joke::COLUMNS).limit(limit) }


end

class Punchline < ActiveRecord::Base
  belongs_to :joke
  belongs_to :user
  has_many :warns
end


class Warn < ActiveRecord::Base
  belongs_to :punchline
  belongs_to :user
end

Схема:

create_table "jokes", :force => true do |t|
    t.text     "content"
    t.integer  "user_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "up_votes",    :default => 0,     :null => false
    t.integer  "down_votes",  :default => 0,     :null => false
    t.string   "cached_slug"
    t.integer  "popularity"
    t.boolean  "anonymous",   :default => false
    t.string   "shorten_url"
  end

  create_table "punchlines", :force => true do |t|
    t.text     "content"
    t.integer  "user_id"
    t.integer  "joke_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "up_votes",    :default => 0,     :null => false
    t.integer  "down_votes",  :default => 0,     :null => false
    t.string   "cached_slug"
    t.boolean  "anonymous",   :default => false
  end

  create_table "warns", :force => true do |t|
    t.integer "punchline_id"
    t.integer "user_id"
    t.integer "weight"
  end
end

Ответы [ 3 ]

3 голосов
/ 24 октября 2011

Я думаю, вам лучше подойти к этому, создав поле warn_level на punchlines.

Так же, как counter, который магически дает вам ActiveRecord, мы можем сделать нечто подобное.

add_column :punchlines, :warn_level, :integer, :default => 0

class Punchline < ActiveRecord::Base
  def update_warn_level!
    self.update_attribute(:warn_level, self.warns.sum(:weight))
  end
end 

Когда добавляется предупреждение, вы можете вручную вызвать этот метод или сделать так, чтобы наблюдатель сделал это за вас.

http://guides.rubyonrails.org/active_record_validations_callbacks.html#observers

class WarnObserver < ActiveRecord::Observer
  def after_create(model)
    if model.punchline
      model.punchline.update_warn_level!
    end
  end
end

# in your application.rb
config.active_record.observers = :warn_observer

После этого ваша проблема станет намного проще, мы можем сделать то, что вы хотите с помощью следующего sql.

SELECT jobs.*, (
  SELECT COUNT(*) FROM punchlines
  WHERE punchlines.job_id = jobs.id
  AND punchlines.warn_level <= 1
) AS punchline_count

Это можно выразить в ActiveRecord

PUNCHLINE_COUNT = <<-SQL
  SELECT COUNT(*) FROM punchlines
  WHERE punchlines.job_id = jobs.id
  AND punchlines.warn_level <= 1
SQL

def with_punchline_count
  select("jobs.*, (#{PUNCHLINE_COUNT}) AS punchline_count")
end

Это выглядит довольно грязно, но я думаю, что вы поставили перед собой сложную проблему.

Надеюсь, это работает для вас.

Примечание: вы также можете кэшировать post_count как столбец, следуя аналогичному подходу. Но давайте разберемся с этим итеративно.

2 голосов
/ 24 октября 2011

Ваш запрос может выглядеть следующим образом. Я заполнил, где ваш вопрос не был ясен.

SELECT j.id, j.joke, j.created_at, sum(w.weight) AS warn_level
FROM   jokes j
LEFT   JOIN punchlines p ON p.joke_id = j.id
LEFT   JOIN warns w ON w.punchline_id = p.id
GROUP  BY 1, 2, 3
HAVING COALESCE(sum(w.weight), 0) <= 1
ORDER  BY j.created_at DESC
LIMIT  my_limit;

Редактировать после получения дополнительной информации

Адаптированные имена таблиц и имена первичных ключей (автоматически генерируемые первичные ключи в ActiveRecord называются id). Остальное было прямым попаданием. :)

Теперь я использую LEFT JOIN для защиты от случаев, когда у jokes нет punchlines или punchlines вообще нет warns. Следовательно, предложение HAVING должно поймать этот случай с помощью COALESCE (), или jokes будет исключено только из-за отсутствия линии удара.
С другой стороны, может быть, они должны ? Нет изюминки? Что за шутка?!

Относительно GROUP BY

Вплоть до PostgreSQL 9.0 вам нужно было перечислить все разгруппированные столбцы вашего SELECT в предложении GROUP BY. Начиная с PostgreSQL 9.1 , вы можете просто написать GROUP BY j.id, чтобы охватить все столбцы таблицы jokes (id является первичным ключом).
Я написал больше об этой теме здесь: Возможно ли иметь SQL-запрос, который использует функции AGG таким образом?

1 голос
/ 27 октября 2011

Я думаю, Вы имели в виду это:

select j.id, j.created_at, j.joke, count(helper.joke_id) as punchline_count
from jokes j
left join(
    select pl.joke_id
    from punchlines pl
    left join warns w on w.punchline_id=pl.id
    group by 1
    having colaesce(sum(w.weight),0)<=1
) as helper on helper.joke_id=j.id
group by 1,2,3
order by 2 desc
limit SOME_LIMIT
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...