2013-01-17 26 views
9

Cách tốt nhất để triển khai chèn/cập nhật nguyên tử cho mô hình có bộ đếm trong Rails là gì? Sự tương tự tốt cho vấn đề tôi đang cố gắng giải quyết là một bộ đếm "như" với hai trường:chèn hoặc tăng nguyên tử trong ActiveRecord/Rails

url : string 
count : integer 

Khi chèn, nếu hiện tại không có bản ghi với url phù hợp, một bản ghi mới sẽ được tạo với số 1; nếu không, trường count của bản ghi hiện tại sẽ được tăng lên.

Ban đầu tôi đã cố gắng mã như sau:

Like.find_or_create_by_url("http://example.com").increment!(:count) 

Nhưng gì ngạc nhiên khi các SQL kết quả cho thấy rằng SELECT xảy ra bên ngoài UPDATE giao dịch:

Like Load (0.4ms) SELECT `likes`.* FROM `likes` WHERE `likes`.`url` = 'http://example.com' LIMIT 1 
(0.1ms) BEGIN 
(0.2ms) UPDATE `likes` SET `count` = 4, `updated_at` = '2013-01-17 19:41:22' WHERE `likes`.`id` = 2 
(1.6ms) COMMIT 

Có một thành ngữ Rails để đối phó với này, hoặc tôi cần phải thực hiện điều này ở cấp độ SQL (và do đó mất một số tính di động)?

+0

4 năm sau, vẫn đang tìm câu trả lời phù hợp/được cộng đồng hỗ trợ. –

Trả lời

5

Tôi không biết về bất kỳ phương thức ActiveRecord nào thực hiện số gia tăng nguyên tử trong một truy vấn. Câu trả lời của riêng bạn là xa như bạn có thể nhận được.

Vì vậy, sự cố của bạn có thể không giải quyết được bằng ActiveRecord. Hãy nhớ rằng: ActiveRecord chỉ là một người lập bản đồ để đơn giản hóa một số thứ, trong khi làm phức tạp người khác. Một số vấn đề chỉ có thể giải quyết được bằng các truy vấn SQL đơn giản tới cơ sở dữ liệu. Bạn sẽ mất một số tính di động. Ví dụ dưới đây sẽ làm việc trong MySQL, nhưng theo như tôi biết, không phải trên SQLlite và những người khác.

quoted_url = ActiveRecord::Base.connection.quote(@your_url_here) 
::ActiveRecord::Base.connection.execute("INSERT INTO likes SET `count` = 1, url = \"#{quoted_url}\" ON DUPLICATE KEY UPDATE count = count+1;"); 
+1

Tôi đã kết thúc bằng cách sử dụng một biến thể của giải pháp của bạn (quoted_url được trích dẫn hai lần). Điều này đã kết thúc được khoảng 5x nhanh hơn so với bắt ActiveRecord :: RecordNotUnique. Có vẻ như một cái gì đó Rails nên cung cấp. –

0

Rails hỗ trợ Transactions, nếu đó là những gì bạn đang tìm kiếm, vì vậy:

Like.transaction do 
    Like.find_or_create_by_url("http://example.com").increment!(:count) 
end 
+3

Thật không may là bản thân nó sẽ không hoạt động. Nếu bạn không có chỉ mục duy nhất trên cột url, bạn có thể nhận được chèn trùng lặp. Nếu bạn có một chỉ mục duy nhất, bạn sẽ nhận được ActiveRecord :: RecordNotUnique.Tôi vừa thử nghiệm cả hai trường hợp bằng cách thêm "sleep 5" vào khối giao dịch. Tôi nghĩ rằng một mức cô lập DB của SERIALIZABLE sẽ được yêu cầu để làm cho công việc này như là-là. –

7

Dưới đây là những gì tôi đã đưa ra cho đến nay. Điều này giả định một chỉ số duy nhất trên cột url. phương pháp increment_counter

begin 
    Like.create(url: url, count: 1) 
    puts "inserted" 
rescue ActiveRecord::RecordNotUnique 
    id = Like.where("url = ?", url).first.id 
    Like.increment_counter(:count, id) 
    puts "incremented" 
end 

Rails' là nguyên tử, và dịch to SQL như sau:

UPDATE 'likes' SET 'count' = COALESCE('count', 0) + 1 WHERE 'likes'.'id' = 1 

Bắt một ngoại lệ để thực hiện logic kinh doanh bình thường có vẻ khá xấu xí, vì vậy tôi sẽ đánh giá cao đề xuất cải tiến.

5

Bạn có thể sử dụng permissive lock,

like = Like.find_or_create_by_url("http://example.com") like.with_lock do like.increment!(:count) end

0

Đối với trường hợp và những người khác sử dụng này, tôi nghĩ update_allapproach là sạch nhất.

num_updated = 
    ModelClass.where(:id    => my_active_record_model.id, 
        :service_status => "queued"). 
      update_all(:service_status => "in_progress") 
if num_updated > 0 
    # we updated 
else 
    # something else updated in the interim 
end 

Ví dụ không chính xác, chỉ cần dán từ trang được liên kết. Nhưng bạn kiểm soát điều kiện where và những gì sẽ được cập nhật. Rất tiện lợi

Các vấn đề liên quan