Re-Wheel 시리즈 1부 - Rails의 find_each는 어떻게 작동하나요?
내 Re-Wheel 시리즈의 첫 번째 게시물에서는 ActiveRecordfind_each에 대해 이야기하고 싶었습니다. 이 방법을 사용하면 데이터베이스 레코드를 배치로 처리할 수 있으므로 앱의 메모리 소비가 줄어들고 수행하는 쿼리의 양이 증가합니다(각 배치당 하나). 모든 프로젝트(Rails 프로젝트뿐만 아니라)에서 메모리 소비에 대해 생각해야 하므로 이 아이디어를 사용 중인 언어나 프레임워크에 포팅할 수 있습니다.
백만 명의 사용자가 있고 모든 이메일을 화면에 인쇄하고 싶다고 가정해 보겠습니다.
User.all.each { |user| puts user.email }
와 같은 작업을 수행할 수 있지만 이렇게 하면 메모리의 모든 레코드가 로드됩니다(좋지 않음). 동등하고 메모리 효율적인 방법은 User.all.find_each { |user| puts user.email }
입니다.그러나 이것이 어떻게 구현됩니까? 음, pagy , kaminari 또는 will_paginate 과 같은 보석을 사용하여 색인 방법에서 페이지 매김을 사용하면 여기에서도 동일한 아이디어가 발생한다는 것을 알 수 있습니다. 그들은 SQL's LIMIT and OFFSET의 힘을 사용하여 매번 데이터. 따라서 100만 명의 사용자 예에서
find_each
는 레코드를 놓치지 않도록 오프셋을 적절하게 변경하면서 1000개의 제한으로 1000,000개의 쿼리를 수행합니다.find_each
메서드에는 또 다른 멋진 점이 있습니다. 보시다시피 강력한 Rubyenumerable 모듈을 사용하여 각 행에서 실행되는 블록을 받습니다.이 방법에는 약간의 제한이 있으며 pluck 방법에서는 작동하지 않습니다. 메모리 사용량을 줄이기 위해 이 작업을 수행하고 있으므로 테이블에서 필요한 열만 선택하는 것이 합리적이므로 구현에 해당 기능을 추가하겠습니다.
내가 작성한 코드를 보여주고 각 부분을 설명하겠습니다.
class DatabaseStream
include Enumerable
DEFAULT_BATCH_SIZE = 1000
def initialize(sql, options = {})
@sql = sql
@pluck = options[:pluck]
@batch_size = options[:batch_size] || DEFAULT_BATCH_SIZE
reset_cursors!
end
def each
if block_given?
while (row = read_from_buffer_or_fetch)
yield row
end
reset_cursors!
else
to_enum(:each)
end
end
private
def reset_cursors!
@gobal_cursor = 0
@buffer_cursor = @batch_size
@buffer = []
end
def read_from_buffer_or_fetch
fetch_data if @buffer_cursor == @batch_size
row = @buffer[@buffer_cursor]
return nil unless row
@gobal_cursor += 1
@buffer_cursor += 1
row
end
def fetch_data
@buffer = @sql.limit(@batch_size).offset(@gobal_cursor)
@buffer = @buffer.pluck(@pluck) if @pluck.present?
@buffer_cursor = 0
end
end
이 코드를 테스트하려면 다음을 실행할 수 있습니다.
query = User.all
stream = DatabaseStream.new(query)
stream.each { |user| puts user.email }
stream = DatabaseStream.new(query, batch_size: 100, pluck: 'id, email') # pluck: %i[id, email] also works
stream.each { |user| puts user[1] } # pluck returns an array
주요 아이디어는 2개의 포인터를 갖는 것입니다. 하나는 배치의 현재 요소에 대한 것이고 다른 하나는 전체 위치(오프셋에 사용됨)에 대한 것입니다.
fetch_data
메서드는 제한 및 오프셋을 설정하고 pluck 매개 변수가 제공된 경우 열을 선택하는 쿼리를 수행합니다. 그런 다음 결과는 메모리의 버퍼에 저장되고 커서가 첫 번째 요소를 가리키도록 설정합니다.인스턴스를 만들 때
@buffer_cursor = @batch_size
를 설정한 다음 read_from_buffer_or_fetch
메서드에서 쿼리를 만들기 전에 이 조건을 확인합니다. 이를 통해 개체를 지연 초기화하여 실제로 필요할 때만 쿼리를 수행할 수 있습니다. 게으름을 달성하는 다른 방법이 있지만 이것이 코드를 깔끔하게 보이게 한다고 생각합니다.보시다시피
Enumerable
모듈을 포함했기 때문에 모든 요소를 반복하는 each
메서드를 구현해야 했습니다. 여기에서 몇 가지 (중요하지 않은) 트릭을 만들었습니다.each
에서 새 열거자를 생성하는 to_enum 메서드를 호출합니다. 이 마지막 부분은 이제 내 열거자를 다른 사람과 연결하고 다음과 같은 작업을 수행할 수 있기 때문에 중요합니다. stream.each.with_index { |row, idx| puts idx }
. 여기까지가 이 Re-Wheel 부분의 전부입니다. 여기에서 한두 가지 요령을 익힐 수 있기를 바랍니다!
Reference
이 문제에 관하여(Re-Wheel 시리즈 1부 - Rails의 find_each는 어떻게 작동하나요?), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/alebian/re-wheel-series-part-1-how-does-rails-findeach-work-m21텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)