관리 화면 목록에 기간 검색을 추가해 달라는 요청을 받았다. 검색할 날짜는 화면에 이미 보이는 값이라 간단해 보였다.
Table of contents
Open Table of contents
상황 — 컬럼이 없었다
order_returns 부터 봤다.
DESC order_returns;
요청일에 해당하는 컬럼이 없었다.
비슷한 이름이 하나 있었다.
claim_reward_hold_at
claim_reward_hold_at 의 값을 보니 화면의 요청일과 다른 승인일이었다.
이름이 비슷하다고 같은 값으로 보고 조건에 넣었으면 조회는 되는데 결과가 틀린다. 오류가 안 나므로 그 틀림이 오래 남았을 것이다.
원인 — 계산으로 붙는 속성
화면이 어디서 값을 받는지 모델을 봤다.
protected $appends = ['return_hold_request_date'];
$appends 에 든 return_hold_request_date 는 컬럼이 아니라 계산해서 붙이는 속성이었다.
계산자를 찾아 읽었다.
public function getReturnHoldRequestDateAttribute()
{
return $this->order->histories()
->where('log', 'LIKE', '%보상/보류 요청%')
->value('created_at');
}
histories 에서 특정 log 패턴을 찾아 그 created_at 을 쓴다.
컬럼이 없다고 그런 값이 없다고 끝냈으면 요청 자체를 반려했을 것이다. 화면에 보이는 이상 어디선가는 오고 있으니 그 출처를 찾아야 했다.
제약 — 정렬도 조건도 못 쓰는 값
계산 속성은 조회 결과를 받은 뒤에 만들어진다.
ORDER BY return_hold_request_date -- 없는 컬럼
WHERE return_hold_request_date >= ? -- 안 됨
MySQL 쪽에서는 그 이름의 존재를 모르므로 ORDER BY 도 WHERE 도 안 된다.
함께 요청받은 정렬 기능도 같은 이유로 불가능했다. 화면에 보인다는 것과 검색할 수 있다는 것이 서로 다른 문제라는 것을 먼저 설명해야 했다.
보이는 값의 출처가 그 값으로 무엇을 할 수 있는지를 정하고 있었다.
조치 — 원본을 조인한다
기간 검색을 하려면 계산의 원본을 조인해야 한다.
$query->whereHas('order.histories', function ($q) use ($from, $to) {
$q->where('log', 'LIKE', '%보상/보류 요청%')
->whereBetween('created_at', [$from, $to]);
});
whereHas 로 histories 에 조건을 직접 건다.
계산자가 하던 일을 조회 쪽으로 옮긴 셈이고 보상/보류 요청 이라는 문자열이 바뀌면 양쪽을 함께 고쳐야 한다.
검증 — 실행 계획
이력 표가 아주 커서 걱정이 됐다.
histories 는 폴리모픽이라 온갖 이력이 다 들어 있고 거기에 LIKE 를 걸면 느릴 것으로 보였다.
복합 인덱스 (historiable_id, historiable_type) 사용
type: ref
type: ref 로 인덱스를 타고 있었다.
historiable_id 로 먼저 좁히고 그 안에서 log 패턴을 거른다. 선행 컬럼으로 좁힌 뒤의 부분 일치는 남은 행 수가 적어서 부담이 안 됐다.
크다는 인상만으로 포기하지 않고 계획을 본 것이 이 판단의 근거였다. 봤더니 정상이라 다른 설계로 돌아갈 이유가 없었다.
변경 내용 — 허용 목록과 분기
기간 검색은 허용 목록 방식이었다.
$allowedPeriodFields = ['created_at', 'completed_at', ...];
allowedPeriodFields 에 넣어야 조건으로 받아들인다.
created_at 쪽은 컬럼 조건인데 이것만 조인이라 분기가 하나 생긴다.
if ($field === 'return_hold_request_date') {
// 이력 조인 경로
} else {
// 컬럼 조건 경로
}
허용 목록은 임의 조건이 들어오는 것을 막아 주는 대신 예외가 하나씩 는다.
이 구조가 왜 나왔는지도 짐작이 된다. 처음에는 요청일이 필요 없어 승인일만 기록했고 나중에 요청일을 보게 됐는데 컬럼을 더해도 지나간 자료를 채울 수 없었다.
컬럼을 더하고 이력에서 채우는 길도 있다. 그러면 정렬과 검색이 정상이 되는데 이력이 없는 오래된 건은 여전히 비고 이후로는 두 곳을 같이 관리하게 된다.
이번 요청의 범위를 넘는 일이라 별도 제안으로 적어 뒀다.
정리
- 화면의 값이 컬럼이 아니라 계산 속성일 수 있다
- 이름이 비슷한 컬럼이 다른 값일 수 있다
- 잘못 넣으면 조회는 되고 결과만 틀린다
- 계산 속성은 정렬도 직접 조건도 안 된다
- 화면에 보이는 것과 검색할 수 있는 것은 다른 문제다
- 보이는 값의 출처가 그 값으로 할 수 있는 것을 정한다
- 검색하려면 원본을 조인한다
- 같은 조건을 두 곳에 쓰게 되므로 함께 고쳐야 한다
- 크다는 인상만으로 포기하지 말고 실행 계획을 본다
- 허용 목록 방식은 추가할 때마다 분기가 생긴다
- 컬럼이 없다고 그런 값이 없다고 끝내지 않는다