제목 | Active Record의 쿼리 캐싱 버그 (+ 수정된 AR 클래스파일) | ||
---|---|---|---|
글쓴이 | 슘님 | 작성시각 | 2013/11/27 09:31:23 |
|
|||
안녕하세요. 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 클래스 파일에 문제가 있다면 지적 부탁 드립니다. |
|||
첨부파일 |
DB_active_rec.zip (7.6 KB) |
||
다음글 | Codeigniter 다운로드 주소.. | ||
이전글 | 멀티업로드에 do_upload를 그대로 이용하기 (2) | ||
한대승(불의회상)
/
2013/11/27 09:58:41 /
추천
0
|
니삼
/
2013/11/27 10:12:27 /
추천
0
ㅠㅠ; 빨리 인수되서 이런 이슈들 처리가 됬으면 좋겠내요 ㅠㅠ
|
슘님
/
2013/11/27 10:14:55 /
추천
0
한대승님// 팁게시판에 올릴까 하다가...원인만 찾았지 아직 해결책을 강구하는 중이라 qna로...ㅎㅎㅎ
|
변종원(웅파)
/
2013/11/27 12:18:58 /
추천
0
포럼에 db캐싱이 적용되어 있는데 저렇게 select에서 반복되는 경우는 없었기에 정상작동을
하고 있습니다. 세세한 디테일이 문제네요. |
letsgolee
/
2013/11/28 12:33:12 /
추천
0
버그라고 하기보다는 복잡한 형태의 쿼리문 작성이라 하는 것이 마땅할 것 같네요. 모든 부분까지 세밀하게 적용하려면 AR의 기능이 너무 커지기 때문에 간단한 정도만 지원하고 만일의 경우를 대비해서 두번째 파라미터를 지원하지 않나요? 제가 볼 때 이렇게 처리하면 될 듯한데...
$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); |
한대승(불의회상)
/
2013/11/28 12:55:54 /
추천
0
letsgolee// 문제해결을 바로보는 다른 방법이군요. ^^
좋은 정보 감사 합니다. |
슘님
/
2013/11/28 13:04:30 /
추천
0
letsgolee // 위의 코드에는 빠져있지만 SQL 내부 함수를 사용하기 위해서 당연히 2번 째 파라미터에 FALSE를 지정합니다. (지적 감사합니다. 빠트렸던 부분이네요. ^^)
그리고 위의 이슈는 escape의 문제가 아닙니다. SQL문을 캐싱할 경우, 쿼리를 수행할 때(get함수 호출 시) 캐싱된 SQL도 쿼리에 추가하기 위하여 _merge_cache 함수를 호출합니다. 그 과정에서 array_unique함수의 오작동으로 'h.qty' 까지 중복 제거 되어집니다. 그 결과 완성된 SQL에서 Syntax 에러가 발생하게 되는게 문제인 것 입니다. SELECT IF(3>2, 'T', 'F'), IF(2>1, 'T', 'F')
위의 SELECT문을 캐싱하신 후 $this->db->get() 해보시면 문제를 발견할 수 있습니다. |
한대승(불의회상)
/
2013/11/28 13:08:23 /
추천
0
슘님// 쿼리를 던지는것 과정에서 생기는 버그가 아니라 캐싱처리 과정에서 생기는 버그 였군요.
버그를 발견해 주시고 해결책까지 제시해 주셔서 정말 감사 합니다. |
milosz
/
2013/11/29 11:50:40 /
추천
0
리포지터리에서 내용을 찾다가보니 파일명이 변경된 모양이네요.
https://github.com/EllisLab/CodeIgniter/blob/develop/system/database/DB_query_builder.php#L2553 |
슘님
/
2013/11/29 11:59:47 /
추천
0
milosz // 구조적으로도 확실히 개선되었네요.
좋은 정보 감사합니다. ^^ |
letsgolee
/
2013/11/30 00:24:09 /
추천
0
@슘님
확실히 문제가 있습니다. 그런데 이건 array_unique를 사용하는 한 또 다른 문제를 야기할 수 있다는 생각이 드네요. 꼭 select에서만 쓰이는 것은 아니니까요. 또한 develop버젼에서도 동일 문제를 생각해 볼 수 있습니다. 확실한 방법은 arrra_unique에서 괄호안에 있는 부분은 unique처리를 하지 않는 방법으로 생각해야 하지 않을까요? 그래서 함수를 만들어 보았습니다. protected function _safe_array_unique($array) { $ret = array(); $cnt = 0; $temp = array(); foreach ($array as $string) { $string = trim($string); if (preg_match_all('/\(/', $string, $matches)) { $cnt += count($matches[0]); } if (preg_match_all('/\)/', $string, $matches)) { $cnt -= count($matches[0]); } if ($cnt) { if (count($temp)) { $ret = array_merge($ret, array_unique($temp)); $temp = array(); } $ret[] = $string; } else { $temp[] = $string; } } // 만일의 경우를 위해 // 있으면 SQL Syntax오류이므로 있어서는 안됨... if (count($temp)) { $ret = array_merge($ret, array_unique($temp)); } return $ret; }이 함수를 이용해서 다음이 부분 array_unique(array_merge($this->$ar_cache_var, $this->$ar_variable));여기를 다음처럼 고치면 됩니다. _safe_array_unique(array_merge($this->$ar_cache_var, $this->$ar_variable));이 방법이 가장 안전한 방법이 될 겁니다. |
캐싱을 잘 사용하지 않아 이러한 버그가 있다는 사실조차 모르고 있었네요.
게시물의 성격이 질문/답변 보다는 팁에 가깝다고 생각되어 이동 하였습니다.