쿼리에 사용자 정의 테이블을 포함하도록 WP_Query를 확장하는 방법은 무엇입니까?


31

나는 지금이 문제에 며칠이 지났습니다. 처음에는 사용자 팔로어 데이터를 데이터베이스에 저장하는 방법이었습니다. 여기에 WordPress Answers에서 몇 가지 멋진 권장 사항이 있습니다. 권장 사항에 따라 다음과 같은 새 테이블을 추가했습니다.

id  leader_id   follower_id
1   2           4
2   3           10
3   2           10

위의 표에서 첫 번째 행에는 ID가 2 인 사용자가 있고 그 뒤에 ID가 4 인 사용자가 있습니다. 두 번째 행에서 ID가 3 인 사용자 뒤에 ID가있는 사용자가 있습니다 세 번째 행에도 동일한 논리가 적용됩니다.

이제 본질적으로 WP_Query를 확장하여 사용자의 리더 만 가져온 게시물을 제한 할 수 있습니다. 따라서 위 표를 고려하여 사용자 ID 10을 WP_Query에 전달하면 결과에는 사용자 ID 2 및 사용자 ID 3의 게시물 만 포함되어야합니다.

답변을 찾으려고 많이 검색했습니다. 또한 WP_Query 클래스를 확장하는 방법을 이해하는 데 도움이되는 자습서를 보지 못했습니다. 비슷한 질문에 대한 Mike Schinkel의 답변 (확장 WP_Query)을 보았지만 내 요구에 적용하는 방법을 실제로 이해하지 못했습니다. 누군가 나를 도와 줄 수 있다면 좋을 것입니다.

요청에 따라 Mike의 답변에 대한 링크 : 링크 1 , 링크 2


Mikes 답변에 대한 링크를 추가하십시오.
카이저

1
무엇을 쿼리 할 것인지에 대한 예를들 수 있습니까? WP_Query게시물을 얻는 데 사용되며 게시물과의 관계를 이해하지 못했습니다.
mor7ifer

@kaiser 나는 Mike의 답변에 대한 링크로 질문을 업데이트했습니다.
John

@ m0r7if3r» "작성자에 의한 게시물 가져 오기"와 유사하게 사용자의 리더 만 가져 오는 게시물을 제한 할 수 있도록 WP_Query를 확장하고 싶습니다.
kaiser

2
@ m0r7if3r Posts는 정확히 내가 쿼리 해야하는 것입니다. 그러나 가져올 게시물은 사용자 정의 테이블에서 특정 사용자의 리더로 나열된 사용자가해야합니다. 즉, WP_Query에 알리고 싶다면 사용자 정의 테이블에 ID가 '10'인 사용자의 리더로 나열된 모든 사용자의 모든 게시물을 가져 오십시오.
John

답변:


13

중요 고지 사항 :이를 수행하는 올바른 방법은 테이블 구조를 수정하는 것이 아니라 wp_usermeta를 사용하는 것입니다. 그런 다음 게시물을 쿼리하기 위해 사용자 지정 SQL을 만들 필요는 없습니다 (단, 관리자 섹션에서 특정 관리자에게보고하는 모든 사람의 목록을 가져 오려면 일부 사용자 지정 SQL이 필요합니다). 그러나 OP가 사용자 지정 SQL 작성에 대해 요청 했으므로 다음은 기존 WordPress 쿼리에 사용자 지정 SQL을 삽입하는 현재 모범 사례입니다.

복잡한 조인을 수행하는 경우에는 join_where 필터를 사용할 수 없습니다. 쿼리의 조인, 선택 및 그룹 별 또는 순서별로 섹션을 수정해야하기 때문입니다.

가장 좋은 방법은 'posts_clauses'필터를 사용하는 것입니다. 이것은 매우 유용한 필터입니다 (남용해서는 안됩니다!). WordPress 코어 내의 많은 코드 줄에 의해 자동으로 생성되는 SQL의 다양한 부분을 추가 / 수정할 수 있습니다. 필터 콜백 서명은 다음 function posts_clauses_filter_cb( $clauses, $query_object ){ }과 같습니다 $clauses.

조항

$clauses다음 키를 포함하는 배열입니다. 각 키는 데이터베이스에 전송 된 최종 SQL 문에서 직접 사용되는 SQL 문자열입니다.

  • 어디에
  • 그룹 별
  • 붙다
  • 주문
  • 뚜렷한
  • 전지
  • 제한

데이터베이스에 테이블을 추가하는 경우 (post_meta, user_meta 또는 taxonomies를 절대 활용할 수없는 경우에만 수행)이 절 중 하나 이상을 터치해야 할 수도 있습니다 fields( 예 : "SELECT" SQL 문의 일부), join( "FROM"절에있는 테이블 이외의 모든 테이블)orderby .

조항 수정

이를 수행하는 가장 좋은 방법 $clauses은 필터에서 얻은 배열 에서 관련 키를 하위 참조하는 것입니다 .

$join = &$clauses['join'];

이제를 수정 $join하면 실제로 직접 수정 $clauses['join']되므로 변경 사항이$clauses 을 반환 할 때 .

원본 절 보존

WordPress에서 생성 한 기존 SQL을 유지하려고 할 가능성이 있습니다 (심지어 들리지 않습니다). 그렇지 않다면 아마도posts_request 필터를 즉, 데이터베이스로 전송되기 직전에 완전한 mySQL 쿼리이므로 자신의 필터로 완전히 클로버 할 수 있습니다. 왜 이렇게 하시겠습니까? 당신은 아마하지 않습니다.

따라서 절에서 기존 SQL을 유지하려면 절에 추가하지 말고 절에 할당하지 마십시오 (예 : $join .= ' {NEW SQL STUFF}';not 사용) $join = '{CLOBBER SQL STUFF}';. $clauses배열 의 각 요소 는 문자열이므로 추가하려는 경우, 다른 문자 토큰 앞에 공백을 삽입하고 싶을 수도 있습니다. 그렇지 않으면 SQL 구문 오류가 발생할 수 있습니다.

각 절에 항상 무언가가 있다고 가정 할 수 있으므로 다음과 같이 공백으로 새 문자열을 각각 시작 $join .= ' my_table해야합니다. 또는 필요한 경우 공백을 추가하는 작은 줄을 항상 추가 할 수 있습니다.

$join = &$clauses['join'];
if (! empty( $join ) ) $join .= ' ';
$join .= "JOIN my_table... "; // <-- note the space at the end
$join .= "JOIN my_other_table... ";


return $clauses;

그것은 다른 어떤 것보다 문체적인 것입니다. 기억해야 할 중요한 점은 이미 일부 SQL이 포함 된 절을 추가하는 경우 문자열 앞에 공백을 남겨 두는 것입니다.

함께 모으기

WordPress 개발의 첫 번째 규칙은 최대한 많은 핵심 기능을 사용하는 것입니다.이것이 향후 작업을 증명하는 가장 좋은 방법입니다. 핵심 팀이 WordPress가 이제 SQLite 또는 Oracle 또는 다른 데이터베이스 언어를 사용할 것이라고 결정한다고 가정하십시오. 직접 작성한 mySQL이 유효하지 않게되어 플러그인이나 테마가 깨질 수 있습니다! WP가 가능한 많은 SQL을 자체적으로 생성하고 필요한 비트를 추가하는 것이 좋습니다.

따라서 비즈니스 우선 순위는 최대한 WP_Query많은 기본 쿼리를 생성 하는 데 활용 됩니다. 이 작업을 수행하는 데 사용하는 정확한 방법은이 게시물 목록이 표시 되는 위치에 따라 다릅니다 . 페이지의 하위 섹션 인 경우 (기본 쿼리가 아님) get_posts(); 기본 쿼리 인 경우 사용 query_posts()하고 완료 할 수 있다고 생각 하지만 적절한 방법은 기본 쿼리가 데이터베이스에 도달하기 전에 차단하고 서버주기를 소비하므로 request필터를 사용하는 것 입니다.

이제 쿼리를 생성했으며 SQL을 만들려고합니다. 사실, 그것은 데이터베이스로 보내지지 않고 만들어졌습니다. posts_clauses필터 를 사용하면 직원 관계 테이블을 믹스에 추가하게됩니다. 이 테이블을 {$ wpdb-> prefix}라고하자. 'user_relationship'이며 교차 테이블입니다. (이런 식으로이 테이블 구조를 일반화하고 'relationship_id', 'user_id', 'related_user_id', 'relationship_type'필드가있는 적절한 교차 테이블로 바꾸는 것이 좋습니다. 훨씬 유연하고 강력합니다. .. 그러나 나는 산만하다.)

내가 원하는 것을 이해하면 리더의 ID를 전달한 다음 해당 리더의 팔로워가 게시 한 게시물 만 보려고합니다. 나는 그 권리를 얻었기를 바랍니다. 그것이 옳지 않다면, 내가 말한 것을 취하여 그것을 당신의 필요에 맞게 조정해야합니다. 테이블 구조를 고수하겠습니다 . a leader_id와 a가 follower_id있습니다. 따라서 JOIN은 {$wpdb->posts}.post_author'user_relationship'테이블의 'follower_id'에 대한 외래 키로 사용됩니다.

add_filter( 'posts_clauses', 'filter_by_leader_id', 10, 2 ); // we need the 2 because we want to get all the arguments

function filter_by_leader_id( $clauses, $query_object ){
  // I don't know how you intend to pass the leader_id, so let's just assume it's a global
  global $leader_id;

  // In this example I only want to affect a query on the home page.
  // This is where the $query_object is used, to help us avoid affecting
  // ALL queries (since ALL queries pass through this filter)
  if ( $query_object->is_home() ){
    // Now, let's add your table into the SQL
    $join = &$clauses['join'];
    if (! empty( $join ) ) $join .= ' '; // add a space only if we have to (for bonus marks!)
    $join .= "JOIN {$wpdb->prefix}employee_relationship EMP_R ON EMP_R.follower_id = {$wpdb->posts}.author_id";

    // And make sure we add it to our selection criteria
    $where = &$clauses['where'];
    // Regardless, you always start with AND, because there's always a '1=1' statement as the first statement of the WHERE clause that's added in by WP/
    // Just don't forget the leading space!
    $where .= " AND EMP_R.leader_id={$leader_id}"; // assuming $leader_id is always (int)

    // And I assume you'll want the posts "grouped" by user id, so let's modify the groupby clause
    $groupby = &$clauses['groupby'];
    // We need to prepend, so...
    if (! empty( $groupby ) ) $groupby = ' ' . $groupby; // For the show-offs
    $groupby = "{$wpdb->posts}.post_author" . $groupby;
  }

  // Regardless, we need to return our clauses...
  return $clauses;
}

13

나는이 질문에 매우 늦게 답변하고 있으며 사과드립니다. 마감일이 너무 바빠서 참석하지 못했습니다.

애플리케이션에서 확장하고 구현할 수있는 기본 솔루션을 제공 한 @ m0r7if3r 및 @kaiser에게 큰 감사를드립니다. 이 답변은 @ m0r7if3r 및 @kaiser가 제공하는 솔루션에 대한 자세한 내용을 제공합니다.

먼저, 왜이 질문이 처음에 왔는지 설명하겠습니다. 질문과 그 의견에서 WP_Query가 주어진 사용자 (팔로어)가 따르는 모든 사용자 (지도자)가 게시물을 가져 오도록 노력하고 있음을 알 수 있습니다. 추종자와 리더 간의 관계는 사용자 정의 테이블에 저장됩니다 follow. 이 문제에 대한 가장 일반적인 해결책은 팔로어의 모든 리더의 사용자 ID를 팔로우 테이블에서 가져 와서 배열에 배치하는 것입니다. 아래를보십시오 :

global $wpdb;
$results = $wpdb->get_results($wpdb->prepare('SELECT leader_id FROM cs_follow WHERE follower_id = %s', $user_id));

foreach($results as $result)
    $leaders[] = $result->leader_id;

리더 배열이 있으면 WP_Query에 인수로 전달할 수 있습니다. 아래를보십시오 :

if (isset($leaders)) $authors = implode(',', $leaders); // Necessary as authors argument of WP_Query only accepts string containing post author ID's seperated by commas

$args = array(
    'post_type'         => 'post',
    'posts_per_page'    => 10,
    'author'            => $authors
);

$wp_query = new WP_Query( $args );

// Normal WordPress loop continues

위의 솔루션은 원하는 결과를 얻는 가장 간단한 방법입니다. 그러나 확장 할 수 없습니다. 수십, 수천 명의 리더를 따르는 팔로워가 생기면 결과적으로 리더 ID 배열이 매우 커져 WordPress 사이트에서 각 페이지로드시 100MB-250MB의 메모리를 사용하여 사이트가 충돌하게됩니다. 이 문제에 대한 해결책은 데이터베이스에서 직접 SQL 쿼리를 실행하고 관련 게시물을 가져 오는 것입니다. 그때 @ m0r7if3r의 솔루션이 구해졌습니다. @kaiser의 권장 사항에 따라 두 구현을 모두 테스트했습니다. WordPress의 새로운 테스트 설치에 등록하기 위해 CSV 파일에서 약 47K 명의 사용자를 가져 왔습니다. 설치 중에 Twenty Eleven 테마가 실행되었습니다. 그런 다음 약 50 명의 사용자가 다른 모든 사용자를 따르도록 for 루프를 실행했습니다. @kaiser와 @ m0r7if3r의 솔루션에 대한 쿼리 시간의 차이는 엄청났습니다. @kaiser의 솔루션은 일반적으로 각 쿼리마다 약 2 ~ 5 초가 걸렸습니다. 내가 생각하는 변형은 WordPress가 나중에 사용하기 위해 쿼리를 캐시 할 때 발생합니다. 반면 @ m0r7if3r의 솔루션은 평균 0.02ms의 쿼리 시간을 보여주었습니다. 두 솔루션을 테스트하기 위해 leader_id 열에 대해 색인을 생성했습니다. 인덱싱이 없으면 쿼리 시간이 크게 증가했습니다.

어레이 기반 솔루션을 사용할 때 메모리 사용량은 약 100-150MB이며 직접 SQL을 실행할 때 20MB로 떨어졌습니다.

추종자 ID를 posts_where 필터 함수에 전달해야 할 때 @ m0r7if3r의 솔루션으로 충돌했습니다. 내가 아는 바에 따르면 WordPress에서는 변수를 파일러 함수에 전달할 수 없습니다. 전역 변수를 사용할 수는 있지만 전역 변수를 피하고 싶었습니다. 결국 문제를 해결하기 위해 WP_Query를 확장했습니다. 그래서 내가 구현 한 최종 솔루션은 다음과 같습니다 (@ m0r7if3r의 솔루션을 기반으로 함).

class WP_Query_Posts_by_Leader extends WP_Query {
    var $follower_id;

    function __construct($args=array()) {
        if(!empty($args['follower_id'])) {
            $this->follower_id = $args['follower_id'];
            add_filter('posts_where', array($this, 'posts_where'));
        }

        parent::query($args);
    }

    function posts_where($where) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'follow';
        $where .= $wpdb->prepare(" AND post_author IN (SELECT leader_id FROM " . $table_name . " WHERE follower_id = %d )", $this->follower_id);
        return $where;
    }
}


$args = array(
    'post_type'         => 'post',
    'posts_per_page'    => 10,
    'follower_id'       => $follower_id
);

$wp_query = new WP_Query_Posts_by_Leader( $args );

참고 : 결국 다음 표에 120 만 항목으로 위의 솔루션을 시도했습니다. 평균 쿼리 시간은 약 0.060ms였습니다.


3
나는이 질문에 대한 토론에 얼마나 감사했는지 말한 적이 없다. 내가 그것을 놓쳤다는 것을 알았으므로, 나는 공감대를 추가했습니다 :)
kaiser

8

posts_where필터를 사용하여 완전히 SQL 솔루션으로이를 수행 할 수 있습니다 . 그 예는 다음과 같습니다.

if( some condition ) 
    add_filter( 'posts_where', 'wpse50305_leader_where' );
    // lol, question id is the same forward and backward

function wpse50305_leader_where( $where ) {
    $where .= $GLOBALS['wpdb']->prepare( ' AND post_author '.
        'IN ( '.
            'SELECT leader_id '.
            'FROM custom_table_name '.
            'WHERE follower_id = %s'.
        ' ) ', $follower_id );
    return $where;
}

나는 이것을 할 수있는 방법이있을 것이라고 생각 JOIN하지만, 나는 그것을 생각해 낼 수 없다. 나는 그것을 가지고 놀고 계속 대답을 업데이트 할 것입니다.

또는 @kaiser가 제안한 것처럼 리더를 가져오고 쿼리하는 두 부분으로 나눌 수 있습니다. 나는 이것이 덜 효율적이라고 생각하지만 확실히 이해하기 쉬운 방법입니다. 중첩 된 SQL 쿼리가 상당히 느려질 수 있으므로 어떤 방법이 더 나은지 결정하기 위해 효율성을 직접 테스트해야합니다.

주석에서 :

함수를 넣고 메소드 가 호출 되기 직전에 functions.php수행해야합니다 . 바로 다음 에 다른 쿼리에 영향을 미치지 않도록 해야 합니다.add_filter()query()WP_Queryremove_filter()


1
A를 수정하고 추가했습니다 prepare(). 편집 내용이 마음에 들지 않기를 바랍니다. 예 : 성능 OP로 측정해야합니다. 어쨌든 : 나는 여전히 이것이 단순히 usermeta 여야한다고 생각합니다.
kaiser

@ m0r7if3r 해결책을 시도해 본 Thx. 가능한 확장 성 문제에 대한 우려와 함께 kaiser의 답변에 대한 의견을 게시했습니다. 고려해주세요.
John

1
@kaiser 최소한 신경 쓰지 마라. 사실 나는 오히려 그것을 고맙게 생각한다 :)
mor7ifer

@ m0r7if3r 감사합니다. 지역 사회 바위에 당신 같은 사람을 갖는 :)
kaiser

1
함수를 넣고 메소드 가 호출 되기 직전에 functions.php수행해야합니다 . 바로 다음 에 다른 쿼리에 영향을 미치지 않도록 해야 합니다. URL 재 작성과 관련된 문제가 무엇인지 잘 모르겠습니다 . 여러 번 사용해 보았지만 결코 보지 못했습니다 ...add_filter()query()WP_Queryremove_filter()posts_where
mor7ifer

6

템플릿 태그

두 기능을 functions.php파일 에 넣으십시오 . 그런 다음 첫 번째 기능을 조정하고 사용자 정의 테이블 이름을 추가하십시오. 그런 다음 결과 배열 내에서 현재 사용자 ID를 제거하기 위해 시도 / 오류가 필요합니다 (주석 참조).

/**
 * Get "Leaders" of the current user
 * @param int $user_id The current users ID
 * @return array $query The leaders
 */
function wpse50305_get_leaders( $user_id )
{
    global $wpdb;

    return $wpdb->query( $wpdb->prepare(
        "
            SELECT `leader_id`, `follower_id`
            FROM %s
                WHERE `follower_id` = %s
            ORDERBY `leader_id` ASC
        ",
        // Edit the table name
        "{$wpdb->prefix}custom_table_name"
        $user_id
    ) );
}

/**
 * Get posts array that contain posts by 
 * "Leaders" the current user is following
 * @return array $posts Posts that are by the current "Leader
 */
function wpse50305_list_posts_by_leader()
{
    get_currentuserinfo();
    global $current_user;

    $user_id = $current_user->ID;

    $leaders = wpse5035_get_leaders( $user_id );
    // could be that you need to loop over the $leaders
    // and get rid of the follower ids

    return get_posts( array(
        'author' => implode( ",", $leaders )
    ) );
}

템플릿 내부

여기에서 원하는 결과를 얻을 수 있습니다.

foreach ( wpse50305_list_posts_by_leader() as $post )
{
    // do something with $post
}

참고 우리는 본건 (가) 위의 추측 게임의 조금 그래서 등 testdata로 있습니다. 있는지 확인하십시오 당신은 우리가 나중에 독자들에게 만족 결과를 가지고, 그래서 당신을 위해 일한 것을이 대답을 편집 할 수 있습니다. 담당자가 너무 적을 경우 수정 사항을 승인하겠습니다. 그런 다음이 메모를 삭제할 수도 있습니다. 감사.


2
JOIN이다 훨씬 더 비싸다. 플러스 : 언급했듯이 테스트 데이터가 없으므로 답변을 모두 테스트하고 결과를 알려주십시오.
카이저

1
WP_Query 자체는 쿼리 할 때 posts 테이블과 postmeta 사이의 JOIN과 함께 작동합니다. PHP 메모리 사용량이 페이지로드 당 70MB-200MB로 증가했습니다. 많은 동시 사용자와 같은 것을 실행하려면 극단적 인 인프라가 필요합니다. 내 생각 엔 WordPress가 이미 유사한 기술을 구현하고 있기 때문에 JOIN은 ID 배열로 작업하는 것과 비교할 때 세금이 적게 듭니다.
John

1
@John은 잘 들었습니다. 실제로 결과물을 알고 싶어합니다.
kaiser

4
여기 테스트 결과가 있습니다. 이를 위해 csv 파일에서 약 47K 명의 사용자를 추가했습니다. 나중에 처음 45 명의 사용자가 다른 모든 사용자를 따르도록 for 루프를 실행했습니다. 그 결과 3,704,951 개의 레코드가 사용자 정의 테이블에 저장되었습니다. 처음에 @ m0r7if3r의 솔루션은 95 초의 쿼리 시간을 제공하여 leader_id 열에서 인덱싱을 켠 후 0.020ms로 줄었습니다. 소비 된 총 PHP 메모리는 약 20MB입니다. 반면, 인덱싱이 설정된 쿼리에서 솔루션은 약 2 ~ 5 초가 걸렸습니다. 소비 된 총 PHP 메모리는 약 117MB입니다.
John

1
주석의 코드 형식이 단순히 짜증나 기 때문에 다른 답변 (처리 및 수정 / 편집 가능)을 추가합니다 : P
kaiser

3

참고 :이 답변은 주석에서 긴 토론을 피하기위한 것입니다.

  1. 주석의 OP 코드는 첫 번째 테스트 사용자 세트를 추가합니다. 실제 사례로 수정해야합니다.

    for ( $j = 2; $j <= 52; $j++ ) 
    {
        for ( $i = ($j + 1); $i <= 47000; $i++ )
        {
            $rows_affected = $wpdb->insert( $table_name, array( 'leader_id' => $i, 'follower_id' => $j ) );
        }
    }

    테스트 정보 OP 이를 위해 csv 파일에서 약 47K 명의 사용자를 추가했습니다. 나중에 처음 45 명의 사용자가 다른 모든 사용자를 따르도록 for 루프를 실행했습니다.

    • 결과적으로 3,704,951 개의 레코드가 사용자 정의 테이블에 저장되었습니다.
    • 처음에 @ m0r7if3r의 솔루션은 95 초의 쿼리 시간을 제공하여 leader_id 열에서 인덱싱을 켠 후 0.020ms로 줄었습니다. 소비 된 총 PHP 메모리는 약 20MB입니다.
    • 반면, 인덱싱이 설정된 쿼리에서 솔루션은 약 2 ~ 5 초가 걸렸습니다. 소비 된 총 PHP 메모리는 약 117MB입니다.
  2. 이 테스트에 대한 나의 대답 :

    보다 "실제"테스트 : 모든 사용자가 a를 따르도록 한 $leader_amount = rand( 0, 5 );다음 $leader_amount x $random_ids = rand( 0, 47000 );각 사용자 의 수를 추가하십시오 . 지금까지 우리가 알고있는 것은 : 사용자가 서로를 따라 가면 내 솔루션이 매우 나쁠 것입니다. 추가 : 테스트를 수행 한 방법과 타이머를 정확히 추가 한 위치를 보여줍니다.

    또한 루프를 함께 계산하는 데 시간이 걸리기 때문에 ↑ 위의 시간 추적은 실제로 측정 할 수 없다고 언급해야합니다. 두 번째 루프에서 결과 ID 세트를 반복하는 것이 좋습니다.

여기에 추가 과정


2
이 Q를 따르는 사람들을위한 참고 사항 : 다양한 조건 하에서 성능을 측정하는 중이며 하루 또는 3 일 내에 결과를 게시 할 것입니다. 생성되었습니다.
John
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.