안녕하세요.
ar을 사용하다 쿼리 캐싱 start_cache(), stop_cache()를 사용 중에 황당한 문제를 발견했습니다.
문제가 발생된 쿼리의 일부분 (설명에 불필요한 부분은 모두 제거하였습니다.)
$this->db->start_cache();
$this->db->select("
SUM(IF(h.dc = 0, h.qty, 0)) AS qty0,
SUM(IF(h.dc = 1, h.qty, 0)) AS qty1,
SUM(IF(h.dc = 2, h.qty, 0)) AS qty2,
SUM(IF(h.dc = 3, h.qty, 0)) AS qty3
", FALSE);
$this->db->stop_cache();
위의 select 쿼리를 캐싱하여 사용할 경우 문제가 발생합니다.
올바른 쿼리문 (정상적으로 출력되어야 하는)
SELECT SUM(IF(h.dc = 0, h.qty, 0)) AS qty0,
SUM(IF(h.dc = 1, h.qty, 0)) AS qty1,
SUM(IF(h.dc = 2, h.qty, 0)) AS qty2,
SUM(IF(h.dc = 3, h.qty, 0)) AS qty3
(중략)
그러나 AR클래스가 아래와 같은 쿼리문으로 만들어 냅니다.
SELECT SUM(IF(h.dc = 0, h.qty, 0)) AS qty0,
SUM(IF(h.dc = 1, 0)) AS qty1,
SUM(IF(h.dc = 2, 0)) AS qty2,
SUM(IF(h.dc = 3, 0)) AS qty3
(중략)
SQL IF문에서 TRUE인 경우에 설정한 필드값인 h.qty가 2~4줄에서 삭제됩니다.
이로 인해 쿼리 수행 에러(Syntax error)가 발생합니다.
AR클래스를 자세히 살펴보니 구조적인 몇 가지 문제가 있더군요.
우선 캐싱된 쿼리를 재사용해주는 함수입니다. -> function _merge_cache()
코드들을 중략하고 핵심 코드만 말씀 드리자면...
protected function _merge_cache()
{
if (count($this->ar_cache_exists) == 0)
{
return;
}
// $val은 'select', 'from' 등의 캐시되는 쿼리 구문명
foreach ($this->ar_cache_exists as $val)
{
$ar_variable = 'ar_'.$val;
$ar_cache_var = 'ar_cache_'.$val;
(중략)
// 문제되는 구문
array_unique(array_merge($this->$ar_cache_var, $this->$ar_variable));
}
(중략)
}
array_unique()에서 문제가 발생하고 있었습니다.
array_unique() 때문에 동일한 이름의 요소 (위의 예제에서는 h.qty)가 제거되고 있더군요.
보충 설명
// Model
$this->db->start_cache();
$this->db->select("a, a, b, c, d");
// AR Class
function select(){
$ar_cache_select = [a, a, b, c, d];
}
function _merge_cache()
{
array_unique(array_merge($ar_cache_select, $ar_select));
// $ar_cache_select = [a, b, c, d]; 로 생성
}
sql문 생성 시 "select a, b, c, d ..."로 생성
function select()
public function select($select = '*', $escape = NULL)
{
if (is_string($select))
{
$select = explode(',', $select);
}
foreach ($select as $val)
{
$val = trim($val);
if ($val != '')
{
$this->ar_select[] = $val;
$this->ar_no_escape[] = $escape;
if ($this->ar_caching === TRUE)
{
$this->ar_cache_select[] = $val;
$this->ar_cache_exists[] = 'select';
$this->ar_cache_no_escape[] = $escape;
}
}
}
return $this;
}
추가적으로 버그는 아니지만 구조적으로 비효율적인 부분도 찾았습니다.
바로
$this->ar_cache_exists 이 부분인데요.
select 구문이 캐시되는가의 여부를 저장하는 부분입니다.
눈치 채신분들은 바로 아실듯...
select 해주는 필드값 들을 콤마(,)로 explode하고 그 수만큼 반복문을 돌리면서 계속 중복(누적) 저장해버립니다.
그만큼 불필요한 연산을 _merge_cache()에서 반복하여 수행하더군요.
(자세한 로직은 해당 함수의 foreach문을 참고해 주세요.)
array_merge를 반복적으로 하기 때문에 array_unique를 사용했고
그로 인해 제 쿼리문 처럼 콤마 사이에 있는 문자열이 동일하고 여러개 있을 경우
(AGE가 20인 사람의 수를 S1에 저장, AGE가 21인 사람의 수를 S2에 저장하는 예:
SELECT SUM(IF(AGE=20, NAME, 0) AS S1, SUM(IF(AGE=21, NAME, 0) AS S2
콤마 사이에 문자열이 동일한 값 발생 : NAME)
같이 제거되는 문제가 발생했습니다.
처음에는 왜 이런가...쿼리 작성 시 문제가 분명 없는데...
고민하다 AR를 버릴까...하다가 버리긴 싫고...ㅎㅎ
쿼리 캐싱을 사용하실 경우 참고되시기 바랍니다.
(저와 같은 경우만 아니면 별 상관은 없네요.
제 CI 버전은 2.1.4입니다.)
AR클래스는 수정하는대로 공유드리도록 하겠습니다.
혹시 뭔가 좋은 소스나 정보가 있다면 공유 부탁 드려요.
_________________________________________________
Related issue:
http://ellislab.com/forums/viewthread/237683/
위의 문제를 해결한 AR 클래스파일 첨부합니다.
[수정 사항]
1. select()
$flag_select 변수 및 관련 분기문 추가
- 원천적으로 $ar_cache_exists를 한 번 만 저장하도록 수정
불필요한 array_unique, array_merge의 반복 수행을 막기 위함
- 캐싱될 경우 $ar_select에 값을 넣지 않도록 수정
어짜피 _merge_cache()에서 캐시값을 $ar_select에 array_merge시킴
(array_merge 시 $ar_select에 중복된 데이터가 들어가지 않도록 방지함)
즉, array_unique할 필요가 없어짐.
추가적으로 array_merge 반복 작업이 없어짐
2. _merge_cache()
$flag_select 변수 및 관련 분기문 추가
- select문의 경우만 단 한번의 array_merge를 하도록 변경
제 생각에는 select문만이 아니라 다른 모든 sql문 처리 함수들이 위의 방식으로 변경되어 한다고 생각합니다.
(그러나 select문을 제외하고는 array_unique로 인한 문제가 생기지 않을 것 입니다.)
array_unique문제가 아닌 처리 구조의 효율성 때문이죠.
수행 시간이 O(n)이라 간결한 코드 vs 효율성+약간의 복잡성 간의 트레이드오프에서 간결한 코드를 선택해도 무리는 없을 것 입니다. (개인적인 견해입니다.)
참고.
// 1번 예제
$this->db->select('a');
$this->db->select('b');
$this->db->select('c');
$this->db->select('d');
// 2번 예제
$this->db->select('a, b, c, d');
2번 예제가 유리합니다.
// 1번 예제
String str = "a";
str += "b";
str += "c";
str += "d";
// 2번 예제
String str = new String("abcd");
내부적으로 수행하는 로직이 위의 개념과 유사하다고 보시면 됩니다.
혹시라도 내용 중 잘못된 부분이나
첨부한 AR 클래스 파일에 문제가 있다면 지적 부탁 드립니다.
캐싱을 잘 사용하지 않아 이러한 버그가 있다는 사실조차 모르고 있었네요.
게시물의 성격이 질문/답변 보다는 팁에 가깝다고 생각되어 이동 하였습니다.