Использование левого соединения и расстановка критериев в разных местах.
Чтобы избежать этого, используя критерии в предложении WHERE для таблицы LEFT JOIN'd, это дало бы эффект, как если бы оно было INNER JOIN.
SELECT
loc.ZIPCode,
COUNT(DISTINCT CASE turns.status WHEN 0 THEN turns.id END) AS turns,
SUM(CASE
WHEN turns.status = 1 AND turns.itemsPurchased > 0
THEN turns.itemsPurchased
ELSE 0
END) AS items
FROM locations loc
LEFT JOIN turns ON turns.locationId = loc.id
WHERE loc.ZIPCode = 12345
GROUP BY loc.id, loc.ZIPCode
Подумайте, что вы здесь делаете, используя фильтры в контроллере, чтобы повлиять на поведение модели при сохранении или обновлении. Я думаю, что гораздо более чистым методом будет обратный вызов before_save
в модели или наблюдателе. Таким образом, вы получаете одно и то же поведение независимо от того, откуда исходит изменение, будь то через контроллер, консоль или даже при запуске пакетных процессов.
Пример:
class Customer < ActiveRecord::Base
NULL_ATTRS = %w( middle_name )
before_save :nil_if_blank
protected
def nil_if_blank
NULL_ATTRS.each { |attr| self[attr] = nil if self[attr].blank? }
end
end
Это дает ожидаемое поведение:
>> c = Customer.new
=> #<Customer id: nil, first_name: nil, middle_name: nil, last_name: nil>
>> c.first_name = "Matt"
=> "Matt"
>> c.middle_name = "" # blank string here
=> ""
>> c.last_name = "Haley"
=> "Haley"
>> c.save
=> true
>> c.middle_name.nil?
=> true
>>
Если вы знаете, какие атрибуты вы хотите закодировать как пустые, вы можете использовать следующее переопределение атрибута:
def colour=(colour)
super(colour.blank? ? nil : colour)
end
Немного громоздко, если у вас есть много атрибутов для покрытия.
Я обобщил ответ и сделал зацепку / расширение, которое можно использовать в качестве инициализатора. Это позволяет использовать его в нескольких моделях. Я добавил его как часть моего репо ActiveRecordHelpers на GitHub
Используйте метод сбора «на месте» (также известный как карта!)
params[:user].collect! {|c| c == "" ? nil : c}
Вы можете использовать гем attribute_normalizer и использовать нормализатор blank , который преобразует пустые строки в нулевые значения.
before_save мне кажется неправильным местом, что если вы хотите использовать значение перед сохранением. Поэтому вместо этого я переопределил сеттеры:
# include through module or define under active_record
def self.nil_if_blank(*args)
args.each do |att|
define_method att.to_s + '=' do |val|
val = nil if val.respond_to?(:empty?) && val.empty?
super(val)
end
end
end
#inside model
nil_if_blank :attr1, :attr2
Просто для полноты я поместил следующее в lib / my_model_extensions.rb
module MyModelExtensions
def self.included(base)
base.class_eval do
def self.nil_if_blank(*args)
args.each do |att|
define_method att.to_s + '=' do |val|
val = nil if val.respond_to?(:empty?) && val.empty?
super(val)
end
end
end
end
end
end
и использую его так: ]
class MyModel
include MyModelExtensions
nil_if_blank :attr1, :attr2
end
Если вы просто хотите убить пропуски, вы можете просто сделать params.delete_if {|k,v| v.blank?}
.
Chris,
Here is a recursive parsing of params that have blanc values.
before_filter :process_params
......
private
def process_params
....
set_blanc_values_to_nil(params)
end
# Maybe move method to ApplicationController
# recursively sets all blanc values to nil
def set_blanc_values_to_nil!(my_hash)
my_hash.keys.each do |key|
val = my_hash[key]
next if val.nil?
my_hash[key] = nil if val.is_a?(String) && val.empty?
set_blanc_values_to_nil!(val) if val.is_a? Hash
end
end
You could do this using inject, which is obvious as to what is happening.
params = params.inject({}){|new_params, kv|
new_params[kv[0]] = kv[1].blank? ? nil : kv[1]
new_params
}
There is also a hack you can do with merge by merging with itself, and passing a block to handle the new value (although this isn't really the intended use for it, but it is more concise)
params.merge(params){|k, v| v.blank? ? nil : v}
Обычно я бы рекомендовал перенести функциональные возможности в модель, как указано в других ответах, это означает, что вы получите такое же поведение независимо от того, откуда происходит изменение.
Однако я не думаю, что в данном случае это правильно. Замеченный эффект сводится исключительно к невозможности кодировать разницу между пустой строкой и значением nil в HTTP-запросе. По этой причине это следует исправить на уровне контроллера. Это также означает, что в других местах все еще можно сохранить в модели пустую строку (что могло быть по законной причине, а в противном случае ее просто покрыть стандартными проверками).
Код I ' m, использующий для решения этой проблемы:
# application_controller.rb
...
def clean_params
@clean_params ||= HashWithIndifferentAccess.new.merge blank_to_nil( params )
end
def blank_to_nil(hash)
hash.inject({}){|h,(k,v)|
h.merge(
k => case v
when Hash : blank_to_nil v
when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
else v == "" ? nil : v
end
)
}
end
...
Я старался сделать код как можно более лаконичным, хотя читабельность несколько пострадала, Итак, вот тестовый пример, демонстрирующий его функциональность:
require "test/unit"
class BlankToNilTest < Test::Unit::TestCase
def blank_to_nil(hash)
hash.inject({}){|h,(k,v)|
h.merge(
k => case v
when Hash : blank_to_nil v
when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
else v == "" ? nil : v
end
)
}
end
def test_should_convert_blanks_to_nil
hash = {:a => nil, :b => "b", :c => ""}
assert_equal( {:a => nil, :b => "b", :c => nil}, blank_to_nil(hash) )
end
def test_should_leave_empty_hashes_intact
hash = {:a => nil, :b => "b", :c => {}}
assert_equal( {:a => nil, :b => "b", :c => {}}, blank_to_nil(hash) )
end
def test_should_leave_empty_arrays_intact
hash = {:a => nil, :b => "b", :c => []}
assert_equal( {:a => nil, :b => "b", :c => []}, blank_to_nil(hash) )
end
def test_should_convert_nested_hashes
hash = {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => "", :g => "", :h => 5}, :i => "bar"}}
assert_equal( {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => nil, :g => nil, :h => 5}, :i => "bar"}}, blank_to_nil(hash) )
end
def test_should_convert_nested_hashes_in_arrays
hash = {:book_attributes => [{:name => "b", :isbn => "" },{:name => "c", :isbn => "" }], :shelf_id => 2}
assert_equal( {:book_attributes => [{:name => "b", :isbn => nil},{:name => "c", :isbn => nil}], :shelf_id => 2}, blank_to_nil(hash))
end
def test_should_leave_arrays_not_containing_hashes_intact
hash = {:as => ["", nil, "foobar"]}
assert_equal( {:as => ["", nil, "foobar"]}, blank_to_nil(hash))
end
def test_should_work_with_mad_combination_of_arrays_and_hashes
hash = {:as => ["", nil, "foobar", {:b => "b", :c => "", :d => nil, :e => [1,2,3,{:a => "" }]}]}
assert_equal( {:as => ["", nil, "foobar", {:b => "b", :c => nil, :d => nil, :e => [1,2,3,{:a => nil}]}]}, blank_to_nil(hash))
end
end
Затем это можно использовать в контроллере следующим образом:
...
@book.update_attributes(clean_params[:book])
...