미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
직장에서 과부하 모델과 컨트롤러의 엄청난 수의 호출을 꽤 많이 접했을 가능성이 큽니다. 이 글에서는 Rails 환경에 대한 지식을 바탕으로 이 문제에 대한 간단한 해결책을 제안하려고 합니다.
레일즈 애플리케이션에서 매우 중요한 측면은 중복 종속성의 수를 최소화하는 것이므로, 최근 전체 레일즈 환경에서 서비스 객체 접근 방식과 PORO(Pure Old Ruby Object) 방법의 사용을 장려하고 있는 이유입니다. 이러한 솔루션을 사용하는 방법에 대한 설명은 다음과 같습니다. 여기. 이 기사에서는 개념을 단계별로 해결하고 문제에 맞게 조정할 것입니다.
가상의 애플리케이션에서 우리는 복잡한 트랜잭션 시스템을 다루고 있습니다. 각 트랜잭션을 나타내는 저희 모델에는 데이터를 가져오는 데 도움이 되는 일련의 범위가 있습니다. 한 곳에서 찾을 수 있기 때문에 업무 효율성이 뛰어납니다. 그러나 이것은 오래 가지 않습니다. 애플리케이션의 개발과 함께 프로젝트 는 점점 더 복잡해지고 있습니다. 범위에는 더 이상 단순한 '위치' 참조가 없고, 데이터가 부족하며, 관계를 로드하기 시작합니다. 잠시 후, 복잡한 거울 시스템을 떠올리게 합니다. 그리고 더 나쁜 것은 여러 줄의 람다를 수행하는 방법을 모른다는 것입니다!
아래에서 이미 확장된 애플리케이션 모델을 확인할 수 있습니다. 결제 시스템 트랜잭션이 저장됩니다. 아래 예시에서 볼 수 있듯이:
트랜잭션 클래스 { where(visible: true) }
scope(:active, 람다 do
joins(<<-SQL
좌측 외부 조인 source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
end)
end
모델도 문제지만 프로젝트의 규모가 커지면 컨트롤러도 늘어나기 시작합니다. 아래 예시를 살펴보겠습니다:
트랜잭션 컨트롤러 클래스 < 애플리케이션 컨트롤러
def index
트랜잭션 = 트랜잭션.for_publishers
.active
.visible
.joins("LEFT JOIN withdrawal_items ON withdrawal_items.transaction_id = transactions.id")
.joins("LEFT JOIN 출금 ON 출금.id = 출금_항목.출금_id 또는
(withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
.order(:created_at)
.page(params[:page])
.per(params[:page])
트랜잭션 = 적용_필터(@트랜잭션)
end
end
여기에서는 여러 곳에서 수행하지 않으려는 추가 조인과 함께 여러 줄의 연결된 메서드를 볼 수 있지만, 이 특정 메서드에서만 볼 수 있습니다. 첨부된 데이터는 나중에 GET 매개 변수를 기반으로 적절한 데이터 필터링을 추가하는 apply_filters 메서드에서 사용됩니다. 물론 이러한 참조 중 일부를 범위로 전송할 수 있지만, 이것이 우리가 실제로 해결하려고 하는 문제가 아닐까요?
우리는 이미 우리가 가진 문제에 대해 알고 있으므로 이 문제를 해결해야 합니다. 서론의 참조를 바탕으로 여기서는 PORO 접근 방식을 사용하겠습니다. 이 경우 이 접근 방식을 쿼리 객체라고 하며, 이는 서비스 객체 개념을 발전시킨 것입니다.
프로젝트의 앱 디렉터리에 "services"라는 새 디렉터리를 만들어 보겠습니다. 여기에 다음과 같은 이름의 클래스를 생성합니다. 트랜잭션 쿼리
.
트랜잭션 쿼리 클래스
끝
다음 단계로, 객체의 기본 호출 경로가 생성될 이니셜라이저를 만들어야 합니다.
트랜잭션 쿼리 클래스
def initialize(scope = Transaction.all)
범위 = 범위
end
end
덕분에 활성 레코드에서 우리 시설로 관계를 전송할 수 있습니다. 이제 제시된 컨트롤러에만 필요한 모든 범위를 클래스로 전송할 수 있습니다.
트랜잭션 쿼리 클래스
def initialize(scope = Transaction.all)
범위 = 범위
end
private
def active(scope)
scope.joins(<<-SQL
좌측 외부 조인 source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
end
def visible(scope)
scope.where(visible: true)
end
def for_publishers(scope)
scope.select("transactions.*")
.joins(:account)
.where("accounts.owner_type = '퍼블리셔'")
.joins("JOIN publishers ON owner_id = publishers.id")
end
end
우리는 여전히 가장 중요한 부분, 즉 데이터를 하나의 문자열로 수집하고 인터페이스를 공개하는 것을 놓치고 있습니다. 모든 것을 하나로 묶는 메서드를 "호출"이라고 이름 붙이겠습니다.
정말 중요한 것은 호출의 범위가 있는 @scope 인스턴스 변수를 사용한다는 것입니다.
트랜잭션 쿼리 클래스
...
def call
visible(@scope)
.then(&method(:active))
.then(&method(:for_publishers))
.order(:created_at)
end
private
...
end
전체 수업은 다음과 같이 구성됩니다:
트랜잭션 쿼리 클래스
def initialize(scope = Transaction.all)
범위 = 범위
end
def call
visible(@scope)
.then(&method(:active))
.then(&method(:for_publishers))
.order(:created_at)
end
private
def active(scope)
scope.joins(<<-SQL
좌측 외부 조인 source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
end
def visible(scope)
scope.where(visible: true)
end
def for_publishers(scope)
scope.select("transactions.*")
.joins(:account)
.where("accounts.owner_type = '퍼블리셔'")
.joins("JOIN publishers ON owner_id = publishers.id")
end
end
정리가 끝나면 모델이 확실히 가벼워 보입니다. 이제 데이터 유효성 검사와 다른 모델 간의 관계에만 집중할 수 있습니다.
트랜잭션 클래스 < 액티브 레코드::베이스
belongs_to :계정
has_one :withdrawal_item
end
컨트롤러는 이미 우리의 솔루션을 구현했으며 모든 추가 쿼리를 별도의 클래스로 옮겼습니다. 그러나 모델에 없었던 호출은 여전히 해결되지 않은 문제로 남아 있습니다. 몇 가지 변경 후 인덱스 작업은 다음과 같습니다:
트랜잭션 컨트롤러 클래스 < 애플리케이션 컨트롤러
def index
트랜잭션 = 트랜잭션 쿼리.new
.call
.joins("LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id")
.joins("LEFT JOIN 출금 ON 출금.id = 출금_항목.출금_id 또는
(withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
.order(:created_at)
.page(params[:page])
.per(params[:page])
트랜잭션 = 적용_필터(@트랜잭션)
end
end
모범 사례와 규칙을 구현하는 경우, 주어진 문제의 유사한 발생을 모두 대체하는 것이 좋은 아이디어일 수 있습니다. 따라서 SQL 쿼리를 인덱스 작업에서 별도의 쿼리 개체로 옮깁니다. 이것을 트랜잭션 필터링 가능 쿼리
클래스입니다. 수업을 준비하는 스타일은 다음과 유사합니다. 트랜잭션 쿼리
. 의 일환으로 코드 가 변경되면 여러 줄의 문자열을 사용하여 보다 직관적인 대용량 SQL 쿼리 레코드가 밀수됩니다. heredoc. 사용 가능한 솔루션은 아래에서 확인할 수 있습니다:
트랜잭션 필터링 쿼리 클래스
def initialize(scope = Transaction.all)
범위 = 범위
end
def call
withdrawal(@scope).then(&method(:withdrawal_items))
end
private
def withdrawal(scope)
scope.joins(<<-SQL
LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
(withdrawals.id = source.resource_id AND source.resource_type = '출금')
SQL
end
def withdrawal_items(scope)
scope.joins(<<-SQL
LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id
SQL
end
end
컨트롤러가 변경된 경우 쿼리 객체를 추가하여 줄의 질량을 줄입니다. 페이지 매김을 담당하는 부분을 제외한 모든 부분을 분리하는 것이 중요합니다.
트랜잭션 컨트롤러 클래스 < 애플리케이션 컨트롤러
def index
트랜잭션 = 트랜잭션 쿼리.new.call.then do |scope|
트랜잭션 필터링 쿼리.new(scope).call
end.page(params[:page]).per(params[:page])
트랜잭션 = 적용_필터(@트랜잭션)
end
end
쿼리 객체는 SQL 쿼리 작성 방식에서 많은 변화를 가져옵니다. ActiveRecord에서는 모든 것이 한 곳에 있기 때문에 모든 비즈니스 및 데이터베이스 로직을 모델에 배치하기가 매우 쉽습니다. 이는 소규모 애플리케이션에 매우 효과적입니다. 프로젝트의 복잡성이 증가함에 따라 로직을 다른 위치에 설정합니다. 동일한 쿼리 객체를 사용하면 멤버 쿼리 쿼리를 특정 문제로 그룹화할 수 있습니다.
덕분에 나중에 코드를 쉽게 상속할 수 있으며 덕 타이핑 덕분에 다른 모델에서도 이러한 솔루션을 사용할 수 있습니다. 이 솔루션의 단점은 코드의 양이 많아지고 책임 소재가 세분화된다는 점입니다. 그러나 이러한 도전을 받아들일지 여부는 우리에게 달려 있으며 뚱뚱한 모델에 의해 얼마나 심하게 방해를 받느냐에 달려 있습니다.