Skip to content

동적 쿼리

동적 쿼리는 매개변수의 값에 따라 동적으로 쿼리가 변형 되는 것을 말합니다. 일반적으로 동적 쿼리는 SQL 주입(injection)과 같은 보안 취약성을 갖기 때문에 최대한 사용을 자제하는 것이 좋습니다. 하지만 많은 SQL 문장에서 사용하는 IN 절이나 클라이언트로부터 전달되는 조회 조건에 따른 동적인 WHERE 절 구성 등의 상황에서 동적 쿼리를 사용하면 가독성 높고 편리하게 SQL 문장을 작성할 수 있습니다. 하지만 조건절(CASE WHEN 등)을 사용하거나 논리 연산자(AND, OR 등)를 사용하여 쿼리 구성이 가능하다면 동적 쿼리를 피하는 것이 좋습니다.

이 문서와 관련된 예제 코드는 다음 예제를 참조 하십시요.

개요

FoxQuery 는 동적 쿼리를 지원하기 위해 매크로 스크립트를 사용합니다. 매크로 스트립트는 C# 스크립트 코드로서 <statement> 태그 내에 <macro> 태그에 작성합니다.

1
2
3
4
5
<macros>
  <macro name="SEARCH_CONDITION">
    // your C# scritp code
  </macro>
</macros>

매크로 호출을 위해 <text> 태그 내에서 매크로 이름을 $$ 문자로 둘러쌓여야 하며 () 문자를 사용하여 매크로 호출임을 나타내야 합니다. 즉, 위 SEARCH_CONDITION 매크로를 호출하기 위한 매크로 호출 식은 $$SEARCH_CONDITION()$$ 입니다.

<statement id="dynamic">
  <text>
    SELECT product_id, product_name FROM products
    WHERE discontinued = 0 $$SEARCH_CONDITION()$$
  </text>
  <macros>
    <macro name="SEARCH_CONDITION">
      // your C# script code
    </macro>
  </macros>
</statement>

FoxDbAccess 객체가 FoxQuery 로부터 Command 객체를 생성할 때 <text> 태그에서 매크로 호출을 발견하면 주어진 이름의 매크로 스크립트 코드를 수행하고 이 코드가 반환한 문자열로 매크로 호출 식을 치환합니다. 예를 들어 다음과 같이 SEARCH_CONDITION 매크로가 다음과 같이 정의 되어 있다면,

1
2
3
4
5
<macros>
  <macro name="SEARCH_CONDITION">
    return "AND product_name LIKE #product_name#"
  </macro>
</macros>

다음과 동등한 FoxQuery 를 사용하는 것과 같습니다. SEARCH_CONDITION 매크로 호출이 매크로 수행 결과 값으로 치환된 것을 알 수 있습니다.

1
2
3
4
5
6
<statement id="dynamic">
  <text>
    SELECT product_id, product_name FROM products
    WHERE discontinued = 0 AND product_name LIKE #product_name#
  </text>
</statement>

실제 상황에서 유용한 동적 쿼리는 매개변수 인자 값에 따라 조건절이 동적으로 생성되는 쿼리일 것입니다. FoxQuery 매크로는 다음과 같이 매크로를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
<macros>
  <macro name="SEARCH_CONDITION">
    <![CDATA[
    if (env.Args.product_name != null)
      return "AND product_name LIKE #product_name#";
    ]]>
  </macro>
</macros>

SEARCH_CONDITION 매크로는 사용자(개발자)가 전달한 매개변수 product_name 인자값이 null 이 아닌 경우에만 LIKE 연산자를 사용하여 조건을 추가합니다. 즉, FoxQuery를 호출하는 코드에서 매개변수 product_name 의 인자값을 어떻게 제공하느냐에 따라 쿼리가 동적으로 변경되고 수행됩니다.

1
2
3
4
FoxDbAccess dbAccess = FoxDbAccess.CreateDbAccess();
var parameters = new { product_name = "%Queso%" };
// var parameters = new { product_name = (string?)null };
DataSet ds = dbAccess.ExecuteQueryDataSet("sample3.dynamic", parameters);

env 객체

env 객체는 FoxQuery에 의해 매크로 스크립트에 제공되는 전역 객체 입니다. 스크립트 코드는 env 객체를 통해 FoxQuery의 <parameter> 태그(들)를 통해 정의된 매개변수들과 FoxQuery 를 호출하는 코드에서 제공된 매개변수 인자값들에 접근이 가능합니다.

Information

env 객체는 NeoDEEX.Data.Query.Script 네임스페이스의 FoxQueryScriptEnvironment 타입에 의해 구현됩니다. 이 타입은 public 타입이지만 개발자가 직접 이 타입을 이용해야할 상황은 거의 없습니다.

env.Args 컬렉션

매크로 스크립트 코드는 env.Args 컬렉션 객체를 통해 FoxQuery 를 호출하는 코드가 제공한 매개변수들에 접근할 수 있습니다.

Information

env.Args 컬렉션은 NeoDEEX.Data.Query.Script 네임스페이스의 FoxQueryScriptArgument 타입에 의해 구현됩니다. 이 타입은 public 타입이지만 개발자가 직접 이 타입을 이용해야할 상황은 거의 없습니다.

  • 인덱서 / 속성

    매개변수 인자에 접근하는 방법은 인덱서(indexer)를 사용하거나 동적 객체(dynamic object) 속성을 사용할 수 있습니다. 예를 들어, 다음과 같이 FoxQuery 를 호출하는 경우,

    1
    2
    3
    4
    5
    6
    7
    8
    FoxDbAccess dbAccess = FoxDbAccess.CreateDbAccess();
    var parameters = new Dictionary<string, object>
    {
        { "param1", "Value1" },
        { "param2", "222222" },
        { "param3", "Value3" },
    };
    string? result = dbAccess.ExecuteQueryScalar("sample3.echo_arg", parameters) as string;
    

    env.Args["param1"] 혹은 env.Args.param1 는 전달된 매개변수 param1 의 인자값을 읽거나 변경할 수 있습니다. 만약 주어진 이름의 매개변수 인자가 존재하지 않는 경우는 null을 반환하며 존재하지 않는 매개변수 인자에 대한 변경은 무시됩니다.

    Information

    위 예제의 경우 매개변수 인자 객체가 Dictionary<string, object> 이지만 DataRow 혹은 Object 매개변수 인자에 대해서도 동일하게 작동합니다.

  • 매개변수 인자값들 열거

    env.Args 컬렉션은 매개변수 인자들에 대한 열거 역시 제공합니다. env.Args 컬렉션은 IEnumerable<string> 인터페이스를 구현하며 이를 통해 매개변수 인자 이름을 열거할 수 있습니다. 따라서 다음 매크로 스크립트는 모든 매개변수 인자를 로그에 기록할 수 있습니다.

    1
    2
    3
    4
    foreach(var key in env.Args)
    {
      env.WriteLog($"{key}={env.Args[key]}");
    }
    
  • 기타 속성/메서드

    env.Args 컬렉션은 이 외에도 다음과 같은 속성/메서드를 제공하여 매크로 스크립트 작성을 용이하게 만듭니다.

    • Count 속성

      매개변수 인자의 개수를 반환합니다.

    • ContainsKey 메서드

      주어진 이름의 매개변수 인자가 존재하는지 반환합니다.

      Note

      앞서 env.Args 컬렉션에 인덱서 혹은 동적 객체 속성을 사용할 때 존재하지 않는 매개변수 인자를 사용하는 경우 null 이 반환된다고 하였습니다. 이러한 상황은 매개변수 인자값을 명시적으로 null 전달한 경우를 구분할 수 없습니다. 따라서 ContainsKey 메서드를 사용하면 매개변수 인자가 제공되지 않은 것인지 매개변수 인자 값이 null 인지 구분이 가능합니다.

    • Add/Remove 메서드

      매개변수 인자 값을 추가/제거하는 메서드 입니다. 매개변수 인자가 Dictionary 인 경우에만 호출 가능하며 DataRow 혹은 Object 매개변수 인자에 대해 이 메서들이 호출되면 InvalidOperationException 예외가 발생합니다.

      1
      2
      3
      env.Args.Add("newArg1", "newValue");
      env.Args.Add("newArg2", 2.2222);
      env.Args.Remove("param1");
      
    • ToDictionary 메서드

      매개변수 인자 객체의 이름/값을 쌍으로 사용하여 Dictionary<string, object?> 객체를 생성하여 반환합니다.

env.Params 컬렉션

매크로 스트립트는 env.Params 컬렉션은 FoxQuery 에서 <parameter> 태그 를 통해 정의된 매개변수 설정들에 접근이 가능합니다.

Information

env.Params 컬렉션은 NeoDEEX.Data.Query.Script 네임스페이스의 FoxQueryScriptParameterCollection 타입에 의해 구현됩니다. 이 타입은 public 타입이지만 개발자가 직접 이 타입을 이용해야할 상황은 거의 없습니다.

  • 인덱서 / 속성

    <parameter> 태그에 의해 정의된 매개변수 설정에 접근하기 위해서는 env.Params 컬렉션의 인덱서(indexer)를 사용하거나 동적 객체(dynamic object) 속성을 사용할 수 있습니다. 인덱서나 속성에 접근하면 매개변수 설정 정보를 담는 FoxQueryParameter 객체를 반환합니다. FoxQueryParameter 객체는 <parameter> 태그와 거의 1 대 1로 대응되는 엔티티 객체로 볼 수 있습니다. 예를 들어 다음과 같은 FoxQuery 정의에 대해서,

    <statement id="dump_params">
      <text>
        ... 생략 ...
      </text>
      <parameters>
        <parameter name="param1" dbType="Varchar" size="30" />
        <parameter name="param2" dbType="Date"/>
      </parameters>
      ...
    </statement>
    

    매크로 스크립트의 env.Params["param1"] 이나 env.Params.param1 코드는 param1 매개변수 설정을 포함하는 FoxQueryParameter 객체를 반환합니다. 예를 들어 다음과 같은 스크립트 코드는 로그에 param1 매개변수 정보와 param2 매개변수 정보를 표시합니다.

    1
    2
    3
    4
    5
    6
    var p1 = env.Params["param1"];
    env.WriteLog($"name={p1.Name} dbType={p1.DbTypeName} size={p1.Size}");
    // result: name=param1 dbType=Varchar size=30
    var p2 = env.Params.param2;
    env.WriteLog($"name={p2.Name} dbType={p2.DbTypeName} size={p2.Size}");
    // result: name=param2 dbType=Date size=
    

    위 매크로 스크립트 코드에서 주목할 부분은 param2 매개변수의 Size 속성입니다. <parameter name="param2" ...> 에서 size 속성이 설정되지 않았기 때문에 FoxQueryParameter 객체의 Size 속성도 설정되지 않습니다(null). 즉, env.Params 컬렉션이 제공하는 DB 매개변수 정보는 <parameter> 태그에서 설정된 정보 그대로 입니다.

    Note

    <parameter> 태그에서 필수가 아닌 선택적인 속성은 FoxQueryParameter 타입의 속성 역시 설정되지 않은 경우 null 을 반환합니다(Nullable<T> 타입 혹은 레퍼런스 타입).

    매개변수 속성을 변경하는 것도 가능합니다. 하지만 이 변경 사항은 현재 수행하는 쿼리에만 적용되며 FoxQuery 자체에 적용되지 않습니다.

    env.Params.param1.Size = 64;
    
  • 매개변수 정의들 열거

    env.Params 컬렉션 역시 IEnumerable<FoxQueryParameter> 인터페이스를 구현하며 이를 통해 FoxQueryParameter 객체들을 열거할 수 있습니다. 따라서 다음과 같은 스크립트 코드를 작성하여 매개변수 정보를 로그에 기록할 수 있습니다.

    1
    2
    3
    4
    foreach(var p in env.Params)
    {
      env.WriteLog($"{p.Name} dbType={p.DbTypeName} size={p.Size}");
    }
    
  • 매개변수 추가/제거

    env.Params 컬렉션은 Add 메서드와 Remove 메서드를 통해 동적으로 DB 매개변수를 추가/제거할 수 있습니다. 이 두 메서드를 사용하여 매크로 스크립트는 다양한 조건이나 로직에 따라서 <parameters> 태그에 추가된 DB 매개변수를 제거하거나 새로운 DB 매개변수를 추가할 수 있습니다.

    1
    2
    3
    4
    5
    6
    if (env.Args.product_name != null)
    {
      env.Params.Add("product_name", "Varchar");
      env.Parmas.product_name.size = 128;
      return "AND product_name LIKE #product_name#";
    }
    

    Note

    FoxQuery 는 # 문자로 둘러쌓인 DB 매개변수 이름을 발견하면 <parameter> 태그에서 DB 매개변수 정보를 찾습니다. 만약 발견되지 않으면 기본 설정을 사용하여 DB 매개변수를 자동으로 추가합니다. 따라서 기본 설정을 사용하는 DB 매개변수를 추가하기 위해 env.Params 컬렉션의 Add 메서드를 호출할 필요는 없습니다. 하지만 위 스크립트 코드 조각처럼 dbType, size 속성을 설정하고자 한다면 명시적으로 Add 메서드를 호출하고 속성을 지정해야 합니다.

로깅

매크로 스크립트의 로깅을 위해 env 객체는 Log 속성을 제공하며 이 속성은 IFoxLog 인터페이스 입니다. 아무런 설정이 없다면 Log 속성이 나타내는 로거는 대체 로거(Fallback logger)로서 아무런 로그도 남기지 않습니다. env.Log 를 통해 로그를 기록하고자 한다면 구성 설정에서 FoxQueryMapper 설정을 수행해야 합니다. queryMappers 설정에서 scriptLogger 속성에 로거의 이름을 명시하고 logging 설정에서 해당 이름을 가진 로거를 구성하면 됩니다. 다음 구성 설정은 매크로 스크립트가 env.Log 속성을 통해 기록하는 로그가 콘솔에 출력되도록 합니다.

{
  "$schema": "https://neodeex.github.io/doc/neodeex.config.schema.json",
  "database": {
    "connectionStrings": {
      "PostgreSQL": {
        "type": "NeoDEEX.Data.NpgsqlClient.FoxNpgsqlDbAccess",
        "connectionString": "...your connection string...",
        "queryMapper": "MyMapper"
      }
    },
    "queryMappers": {
      "MyMapper": {
        "directories": [ "./foxml" ],
        "scriptLogger": "MacroLogger"
      }
    }
  },
  "logging": {
    "loggers": {
      "MacroLogger": {
        "filter": "Verbose",
        "providerType": "NeoDEEX.Diagnostics.Loggers.FoxConsoleLoggerProvider"
      }
    }
  }
}

매크로 스크립트 코드는 C# 코드이지만 디버거를 사용하여 디버깅하는 기능은 지원되지 않습니다. 따라서 현재 유일한 디버깅 도구는 스크립트 코드에서 사용가능한 로깅 뿐입니다.

  • WriteLog 메서드

    env 객체는 로그를 보다 편리하게 남길 수 있도록 WriteLog 메서드를 제공합니다. WriteLog 메서드는 구성된 로거에 수행되는 FoxQuery 의 고유의 쿼리 ID, 매크로 이름을 포함하여 주어진 메시지를 Verbose 수준으로 기록합니다.

    public void WriteLog(string message);
    

    다음은 sample3.dump_params 쿼리 ID를 가진 FoxQuery 에서 DUMP_PARAMS 매크로가 기록한 로그의 예입니다.

    1
    2
    3
    [MacroLogger] sample3:dump_params.DUMP_PARAMS()> name=param2 dbType=Date size=
    [MacroLogger] sample3:dump_params.DUMP_PARAMS()> param1 dbType=Varchar size=30
    [MacroLogger] sample3:dump_params.DUMP_PARAMS()> param2 dbType=Date size=
    
  • Trace 메서드

    env.Trace 메서드는 디버거에 주어진 메시지를 출력합니다.

    public void Trace(string message);
    

    다음은 디버거에 출력되는 Trace 메시지의 예를 보여 줍니다.

    [FQMS_Trace] Test log #1...
    [FQMS_Trace] Test log #2...
    

헬퍼 메서드들

FoxQuery는 몇가지 헬퍼 메서드들을 제공하여 특정 상황에서 매크로 스크립트를 보다 편리하게 작성할 수 있도록 해 줍니다. 이들 헬퍼 메서드들은 ' 문자를 문자열 앞뒤에 추가해주는 단순한 메서드들 부터 IN 절, SET 절, VALUES 절을 자동으로 작성해 주는 고급 메서드들을 포함합니다.

문자열 관련 메서드

  • Quoted 확장 메서드

    주어진 문자열 앞뒤에 SQL 문자열을 의미하는 ' 문자를 추가한 문자열을 반환합니다.

    string s = "keyword".Quoted();
    // result: 'keyword'
    
  • SurroundWith 확장 메서드

    주어진 문자열 앞/뒤를 주어진 문자로 감싼 문자열을 반환합니다. 주로 LIKE 절에 % 문자를 포함하는 조건을 작성할 때 사용할 수 있습니다.

    1
    2
    3
    4
    5
    6
    string s1 = "keyword".SurroundWith('%');
    // result: %keyword%
    string s2 = "keyword".SurroundWith('%').Quoted();
    // result: '%keyword%'
    string s3 = "keyword".SurroundWith('%', true);
    // result: '%keyword%'
    

StringBuilder 관련 헬퍼 메서드

  • AppendQuoted 확장 메서드

    StringBuilder 객체에 ' 문자로 둘러쌓인 문자열을 추가할 때 사용합니다.

    1
    2
    3
    var sb = new StringBuilder();
    sb.Append("product_name = ").AppendQuoted("some_name");
    // result: product_name = 'some_name'
    
  • AppendSurroundWith 확장 메서드

    StringBuilder 객체에 주어진 문자(열)로 둘러쌓인 문자열을 추가할 때 사용합니다. 주로 LIKE 절에 % 문자로 둘러 쌓인 문자열을 추가하거나 SQL 문장에 DB 매개변수를 추가할 때 사용합니다. 다음 매크로 스크립트 코드는 INSERT 문장의 VALUES 절에 DB 매개변수들을 추가하는 예를 보여줍니다.

    1
    2
    3
    4
    5
    6
    7
    8
    var sb = new StringBuilder().Append("VALUES (");
    foreach(var key in env.Args)
    {
      sb.AppendSurroundWith(key, "#", false).Append(", ");
    }
    if (sb.Length > 0) sb.Remove(sb.Length - 2, 2);
    return sb.Append(')').ToString();
    // result 예시: VALUES (#prouct_id#, #product_name#, #unit_price#)
    
  • Trim/TrimStart/TrimEnd 확장 메서드

    StringBuilder 객체의 앞/뒤에서 주어진 문자(열)을 제거(trim)합니다.

    1
    2
    3
    4
    var sb = new StringBuilder().Append("_xx_yy_TheString_yy__");
    sb.TrimStart("_xx_").TrimEnd("__").Trim("yy");
    return sb.ToString();
    // result: _TheString_
    

SQL 헬퍼 메서드

FoxQuery 는 매크로 스크립트가 동적 SQL 문장을 작성하는데 도움이되는 추가적인 메서드들을 NeoDEEX.Data.Query.Script 네임스페이스의 FoxQueryScriptSqlHelper 클래스에 구현해 놓았습니다. 매크로 스크립트에서는 긴 이름 대신 SQL 이라는 별칭 클래스 이름을 사용하여 이들 메서드들을 호출할 수 있습니다.

IN 메서드

IN 메서드는 SQL IN 절을 생성해 줍니다.

public static string IN(object arg, bool quoted = true);

첫번째 매개변수가 배열과 같은 컬렉션(IEnumerable)인 경우 각 값들을 IN 절에 나열하고 그렇지 않은 경우 하나의 값만이 IN 절에 포함됩니다. 다음은 몇가지 IN 메서드 호출 예 입니다.

1
2
3
4
5
6
string inClause1 = SQL.IN(new int[] { 1, 2 });
// result: IN ('1', '2')
string inClause2 = SQL.IN(new int[] { 1, 2, 3, 4 }, false);
// result: IN (1, 2, 3, 4)
string inClause3 = SQL.IN("singleValue");
// result: IN ('singleValue')

다음 FoxQuery 는 IN 메서드를 사용하여 IN 절을 생성하는 구체적인 예를 보여 줍니다.

<statement id="dynamic_in">
  <text>
    SELECT * FROM products WHERE category_id $$IN_CLAUSE()$$
  </text>
  <macros>
    <macro name="IN_CLAUSE">
      <![CDATA[
      return SQL.IN(env.Args.categories, false);
      ]]>
    </macro>
  </macros>
</statement>

위 FoxQuery 를 호출하는 코드는 다음과 같습니다.

1
2
3
4
5
6
FoxDbAccess dbAccess = FoxDbAccess.CreateDbAccess();
var parameters = new Dictionary<string, object>()
{
    { "categories", new int[] {1, 2} },
};
DataSet ds = dbAccess.ExecuteQueryDataSet("sample3.dynamic_in", parameters);

위 코드와 매크로 스크립트에 의해 생성되고 수행되는 SQL 문장은 다음과 같습니다.

SELECT * FROM products WHERE category_id IN (1, 2)

기본 IN 메서드는 코드로부터 전달받은 매개변수 값을 SQL 문장에 직접 삽입하기 때문에 SQL Injection 공격에 취약합니다. 또한 매개변수 값에 따라서 문자열을 나타내는 따옴표(') 사용 여부도 처리하기 까다롭습니다. 이러한 문제점을 해결하기 위해 매개변수화된 IN 절을 생성해주는 메서드들이 제공됩니다.

public static string IN(object arg, FoxQueryScriptEnvironment env);

이 메서드는 IN 절에 사용될 값들을 매개변수화하여 그 매개변수를 env.Params 컬렉션에 추가하며 매개변수 인자값들은 env.Args 컬렉션에 추가합니다. 다음은 몇몇 IN 메서드 호출 결과를 보여줍니다.

1
2
3
4
string inClause1 = SQL.IN(new int[] { 1, 2 }, env);
// result: IN (#_IN_P0_0#, #_IN_P0_1#)
string inClause2 = SQL.IN(new int[] { 1, 2, 3, 4 }, env);
// result: IN (#_IN_P0_0#, #_IN_P0_1#, #_IN_P0_2#, #_IN_P0_3#)

추가되는 매개변수는 _IN_P 라는 기본 이름에 중복 방지를 위해 숫자가 붙으며 또한 한 IN 메서드 호출 내에서 사용된 IN 절의 값 개수 만큼 추가적인 인덱스가 붙게 됩니다. 위 예제의 첫번째 IN 메서드 호출 결과로 _IN_P0_0 매개변수와 _IN_P0_1 매개변수가 env.Params 컬렉션에 추가되며 이들 매개변수 인자값으로 env.Args 컬렉션에 _IN_P0_0 이름을 가진 인자값 1 과 _IN_P0_1 이름을 가진 인자값 2가 추가됩니다.

매개변수화된 IN 절을 사용하는 예제는 다음과 같습니다. FoxQuery 는 매크로 스트립트에서 IN 메서드 호출 부분만을 변경하면 됩니다. FoxQuery 를 호출하는 코드는 변함이 없습니다.

<statement id="dynamic_in">
  <text>
    SELECT * FROM products WHERE category_id $$IN_CLAUSE()$$
  </text>
  <macros>
    <macro name="IN_CLAUSE">
      <![CDATA[
      return SQL.IN(env.Args.categories, env);
      ]]>
    </macro>
  </macros>
</statement>

위 FoxQuery 와 매크로에 의해 최종적으로 생성되고 수행되는 SQL 문장은 다음과 같습니다.

SELECT * FROM products WHERE category_id IN (:_IN_P0_0, :_IN_P0_1)

SET 메서드

SET 메서드는 IDictionary<string, object> 객체로부터 SQL UPDATE 문장의 SET 절을 생성해 주는 헬퍼 메서드 입니다.

public static string SET(IDictionary<string, object?> dic, bool quoted = true);

IDictionary<string, object> 객체의 키/값은 컬럼 이름과 컬럼 값으로 사용됩니다. 다음은 SET 메서드 호출의 예를 보여줍니다.

1
2
3
var dic1 = new Dictinary<string, object> { {"c0", "value0"}, {"c1", 1} };
string setClause = SQL.SET(dic1);
// result: SET c0='value0', c1='1'

앞서 살펴본 IN 메서드와 마찬가지로 이 메서드는 SQL Injection 공격에 매우 취약합니다. 따라서 매개변수화된 SET 절을 생성해 주는 다음 SET 메서드를 사용해야 합니다.

public static string SET(IDictionary<string, object?> dic, FoxQueryScriptEnvironment env);

매개변수화를 수행하는 위 SET 메서드를 호출하면 IDictionary<string, object> 객체의 키/값은 DB 매개변수 이름과 매개변수 인자 값으로 env.Params 컬렉션과 env.Args 컬렉션에 추가됩니다. 예를 들어 다음과 같이 SET 메서드를 호출한다면,

var dic1 = new Dictinary<string, object> { {"c0", "value0"}, {"c1", 1} };
string setClause = SQL.SET(dic1, env);

env.Params 컬렉션에는 c0, c1 매개변수가 추가되며, env.Args 컬렉션에는 c0 이름을 갖는 인자값 value0c1 이름을 갖는 인자값 1 이 추가됩니다. 이러한 매개변수/인자값 추가는 해당 컬렉션에 DB 매개변수/인자값이 존재하지 않는 경우에만 수행됩니다.

다음은 SET 메서드를 활용하여 동적 UPDATE 를 수행하는 FoxQuery 의 예를 보여줍니다.

<statement id="dynamic_update">
  <text>
    UPDATE my_demo_table $$SET_CLAUSE()$$ WHERE col_id = #col_id#
  </text>
  <macros>
    <macro name="SET_CLAUSE">
      <![CDATA[
      var dic = env.Args.ToDictionary();
      dic.Remove("col_id");
      return SQL.SET(dic, env);
      ]]>
    </macro>
  </macros>
</statement>

Note

SET_CLAUSE 매크로에서 col_id 를 제거하지 않으면 SET 절에 col_id 를 변경하는 코드도 작성됩니다. env.Args 컬렉션의 ToDictionary 메서드는 새로운 Dictionary<string, object> 객체를 생성하고 값을 복제하기 때문에 col_id 를 제거하는 작업은 #col_id# DB 매개변수를 바인딩하는데 영향을 주지 않습니다.

다음 코드는 FoxQuery 를 호출하는 예를 보여 줍니다.

1
2
3
4
5
6
7
8
FoxDbAccess dbAccess = FoxDbAccess.CreateDbAccess();
var parameters = new Dictionary<string, object>
{
    { "col_id", 9999 },
    { "col_str", "value_9999" },
    { "col_int", 199991 },
};
dbAccess.ExecuteQueryNonQuery("sample3.dynamic_update", parameters);

dynamic_update FoxQuery 와 위 C# 코드를 통해 생성되고 수행되는 SQL 문장은 다음과 같습니다.

UPDATE my_demo_table SET col_str=:col_str, col_int=:col_int WHERE col_id = :col_id

COLUMNSVALUES 메서드

COLUMNS 메서드와 VALUES 메서드는 INSERT 문장에서 컬럼 목록과 VALUES 절을 생성하는데 도움을 주는 메서드 입니다. 먼저, COLUMNS 메서드는 주어진 문자열 컬렉션을 INSERT 문장에서 컬럼 목록으로 사용할 수 있는 문자열로 반환합니다.

1
2
3
public static string COLUMNS(IEnumerable<string> names);
public static string VALUES(IEnumerable<object?> values, bool quoted = true);
public static string VALUES(IDictionary<string, object?> dic, FoxQueryScriptEnvironment env);

다음은 COLUMNS 메서드와 VALUES 메서드를 호출한 결과를 보여줍니다.

1
2
3
4
5
var dic = new Dictinary<string, object> { {"c0", "value0"}, {"c1", 1}, {"c2", "str2"} };
var columnsStr = SQL.COLUMNS(dic.Keys);
// result: (c0, c1, c2)
var valuesClause = SQL.VALUES(dic.Values)
// result: VALUES ('value0', '1', 'str2')

앞서 IN 메서드와 SET 메서드에서 지적한 것과 동일하게 SQL Injection 위험을 피하기 위해서는 매개변수화된 메서드를 사용해야 합니다. 매개변수화를 수행하는 VALUES 메서드는 SET 메서드와 동일하게 env.Params 컬렉션과 env.Args 컬렉션에 DB 매개변수와 매개변수 인자값을 적절히 추가합니다. 다음은 COLUMNS 메서드와 VALUES 메서드를 사용하여 INSERT를 수행하는 FoxQuery 와 이를 호출하는 C# 코드를 보여줍니다.

<statement id="dynamic_insert">
  <text>
    INSERT INTO my_demo_table $$COLUMNS()$$ $$VALUES_CLAUSE()$$
  </text>
  <macros>
    <macro name="COLUMNS">
      <![CDATA[
      return SQL.COLUMNS(env.Args);
      ]]>
    </macro>
    <macro name="VALUES_CLAUSE">
      <![CDATA[
      return SQL.VALUES(env.Args.ToDictionary(), env);
      ]]>
    </macro>
  </macros>
</statement>
1
2
3
4
5
6
7
8
FoxDbAccess dbAccess = FoxDbAccess.CreateDbAccess();
var parameters = new Dictionary<string, object>
{
    { "col_id", 9999 },
    { "col_str", "value_9999" },
    { "col_int", 199991 },
};
dbAccess.ExecuteQueryNonQuery("sample3.dynamic_insert", parameters);

위 동적 쿼리에 의해 생성되고 수행되는 SQL 문장은 다음과 같습니다.

INSERT INTO my_demo_table (col_id, col_str, col_int) VALUES (:col_id, :col_str, :col_int)

Summary

Fox Query 가 제공하는 동적 쿼리 기능은 스크립트 코드를 사용하는 매크로 방식 입니다. 즉 <macro> 태그와 C# 스크립트 코드를 사용하여 매크로를 작성하고, <text> 태그 내에서 이 매크로를 호출하는 방식을 사용하여 SQL 문장을 동적으로 생성/변경 할 수 있습니다.

매크로 스크립트 지원을 위해 Fox Query 는 env 객체를 통해 쿼리가 수행되는 DB 매개변수 목록(env.Params), 호출 코드가 전달한 매개변수 인자들(env.Args)에 대한 정보를 제공합니다. 또한 IN, SET, COLUMNS, VALUES 메서드와 같은 헬퍼 메서드를 제공하여 동적 쿼리 작성을 편하게 해 줍니다. 다만, 동적 쿼리는 SQL Injection 과 같은 보안 취약점과 SQL 문장 가독성을 낮출 수 있으므로 과도한 사용을 자제해야 합니다.