[MyBatis 소스코드 손으로 뜯어보기] Dynamic SQL 전 과정 분석

동적 SQL 개요

동적 SQL은 MyBatis의 강력한 기능 중 하나로 JAVA 코드에서 SQL 문자열을 조합하는 문제를 제거하는 동시에 SQL에 대한 독립적인 제어를 유지하여 SQL 성능 최적화 및 변환에 더 편리합니다.

동적 SQL에서는 SQL의 조합을 제어하기 위해 XML 스크립트 요소를 사용하는데 일상적인 개발에서 사용되는 요소들인데 함께 살펴보도록 하겠습니다.

  • 만약에
  • (그렇지 않으면) 선택하다
  • 손질 (여기서, 설정)
  • 각각

여기에 이미지 설명 삽입

만약에

<if test="title != null">
    AND title like #{title}
</if>

if 요소에서 테스트를 통해 OGNL 논리식을 받아들여 빈 판단, 크기, 및, 또는, 하위 속성에 대한 계산과 같은 기존 논리 계산에 사용할 수 있습니다.

선택(언제, 그렇지 않으면)

선택은 여러 조건 중 하나를 선택하는 데 사용되며 충족되지 않으면 그렇지 않은 경우 값을 사용합니다. java의 스위치와 유사합니다. 물론 이런 종류의 논리도 if로 구현할 수 있지만 논리 표현이 비교적 복잡합니다. 또한 if 요소에는 해당하는 else 요소가 없습니다.

손질 (어디서, 설정)

trim은 다음 예제와 같이 SQL을 어셈블한 후 추가 SQL 문 문제를 해결하는 데 사용됩니다.

<select id="findBlog"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
   AND state = #{state}
  </if>
</select>

if 조건이 충족되면 문법에 AND 문자가 추가되어 최종적으로 SQL이 생성됩니다.

SELECT * FROM BLOG  WHERE  AND state = #{state}

충족되지 않으면 SQL도 잘못되며 구문에 추가 WHERE가 있습니다.

  SELECT * FROM BLOG  WHERE 

where 요소는 하위 요소가 아무 것도 반환하지 않는 경우에만 "WHERE" 절을 삽입합니다. 또한 절이 "AND" 또는 "OR"로 시작하는 경우 where 요소도 절을 제거합니다. 여기서 요소는 다음 트림 요소와 동일합니다.

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

Set 요소는 여러 필드를 수정할 때 추가 쉼표 문제에 사용되며 다음 trim 요소와 동일합니다.

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

각각

이 요소는 in의 여러 조건을 구성하거나 일괄 처리를 추가 및 수정하는 등 컬렉션 값을 탐색하는 데 사용됩니다. 요소 본문에서 사용할 수 있는 컬렉션 항목(item) 및 인덱스(index) 변수를 선언하여 컬렉션을 지정할 수 있습니다.

OGNL 표현

OGNL의 전체 이름은 Object Graph Navigation Language(객체 그래프 탐색 언어)로 JAVA 표현 언어로 MyBatis에서 동적 SQL 기능을 구현하는 데 사용됩니다.

OGNL의 주요 기능은 객체의 속성에 따라 값을 얻거나 판단하는 것입니다. 객체 속성의 경로에 따라 객체 내부의 값에 쉽게 접근할 수 있습니다.

OGNL이 실현할 수 있는 주요 기능은 다음과 같습니다.

  1. 개체 속성에 액세스: user.name
  2. 호출 방법: user.getName()
  3. 컬렉션 요소 액세스: user.orders[0]
  4. 속성 또는 메소드가 존재하는지 확인: user.name?
  5. 정적 클래스 메서드 또는 속성 호출: @java.lang.Math@PI
  6. 비교 및 산술 연산 구현: user.age > 18
  7. 논리 연산 실현: user.name && user.age < 30

따라서 OGNL은 개체 속성의 값을 조작하고 판단하는 쉬운 방법을 제공합니다. "."객체 속성에 대한 다양한 작업을 쉽게 구현할 수 있습니다 .

MyBatis는 OGNL을 사용하여 동적 SQL의 다양한 기능을 구현합니다.

  1. if: 주어진 OGNL 표현식의 결과가 참인지 확인하고 참이면 현재 요소의 내용을 추가합니다.
    <if test="name != null">
      name = #{name}  
    </if>
    
  2. 선택, 언제, 그렇지 않으면: Java의 switch 문과 동일하며 포함할 요소 콘텐츠는 첫 번째 when의 OGNL 표현식이 충족되는지 여부에 따라 결정됩니다.
    <choose>
      <when test="age > 18">
        age > 18
      </when>
      <when test="age == 18">
        age == 18 
      </when>
      <otherwise> 
        age < 18
      </otherwise>
    </choose> 
    
  3. trim: 문자열의 접두사 또는 접미사를 처리하는 데 사용됩니다. 여기서 prefixOverrides 및 suffixOverrides는 OGNL 표현식을 지원합니다.
    <trim prefix="(" prefixOverrides="(" suffix=")" suffixOverrides=")">...</trim>
    
  4. 여기서: SQL을 동적으로 연결하는 데 사용되는 where 조건문은 자동으로 첫 번째 및 또는 또는를 제거합니다. 그 중 테스트는 OGNL 표현식을 지원합니다.
    <where>
      <if test="name != null"> name = #{name} </if>  
      <if test="age != null"> and age = #{age} </if>
    </where>
    
  5. set: SQL을 동적으로 연결하는 데 사용되는 set 조건문은 자동으로 마지막 쉼표를 제거합니다. 그 중 테스트는 OGNL 표현식을 지원합니다.
    <set>
      <if test="name != null"> name = #{name}, </if>
      <if test="age != null"> age = #{age} </if> 
    </set> 
    
  6. forEach: 컬렉션을 반복하는 데 사용되며 컬렉션의 각 요소를 반복하고 태그 본문 콘텐츠를 실행합니다.
    <forEach items="${lists}" index="index" item="item" open="(" close=")" separator=",">
    #{item}  
    </forEach>
    

OGNL이 MyBatis의 동적 SQL 구현을 강력하게 지원한다는 것을 알 수 있습니다. 간단한 OGNL 표현식을 통해 주어진 조건이 충족되는지 여부를 판단하고 특정 SQL 조각을 포함할지 여부를 결정할 수 있어 MyBatis의 동적 SQL 기능에 큰 유연성을 제공합니다.

따라서 OGNL은 MyBatis 동적 SQL 구현의 초석이며, MyBatis가 단순한 스플라이싱이 아닌 조건에 따라 필요한 SQL 문을 필터링하고 조인할 수 있도록 하여 MyBatis에서 생성된 SQL 문을 매우 유연하고 강력하게 만듭니다.

동시에 도구 클래스도 소개합니다.ExpressionEvaluator

따라서 ExpressionEvaluator는 주로 두 가지 기능을 제공합니다.

  1. OGNL 식 구문 분석: 식 문자열을 식의 내부 의미 구조를 나타내는 Expression 개체로 구문 분석합니다.
  2. OGNL 식 평가: 식 개체 및 지정된 개체 그래프 컨텍스트를 사용하여 식의 최종 결과를 평가합니다.

주로 다음 API를 제공합니다.

  • parseExpression(String expression): OGNL 표현식을 구문 분석하고 Expression을 반환합니다.
  • 평가(문자열 표현식, 객체 루트): 표현식을 직접 구문 분석하고 평가하며 루트 객체는 루트이며 결과를 반환합니다.
  • evaluate(Expression expr, Object root): 표현식 객체 expr 및 루트 객체 루트를 사용하여 표현식을 평가하고 결과를 반환합니다.

이러한 API를 통해 MyBatis는 Mapper.xml의 OGNL 표현식을 Expression으로 파싱한 다음 매개변수 개체를 사용하여 Expression을 평가하여 최종 결과를 얻고 그에 따라 다음 조치를 결정할 수 있습니다. 이를 통해 MyBatis는 OGNL 표현식을 매우 명확하고 유연하게 지원하고 동적 SQL 및 지연 로딩 기능을 실현할 수 있습니다.

BoundSql

소스코드를 보면 BoundSql을 자주 보는데 이게 뭘까요?
여기에 이미지 설명 삽입

BoundSql은 직접 실행 가능한 SQL 문이 아닙니다 . SQL 문에 대한 정보를 캡슐화하는 POJO 클래스입니다.

Mapper에서 메서드를 호출하면 MyBatis는 해당 Mapper.xml 파일을 구문 분석하고 전달한 매개 변수와 동적 SQL 조건에 따라 실행될 최종 SQL 문을 생성합니다. 이 SQL 문은 BoundSql 개체에 캡슐화됩니다.

BoundSql에 포함된 정보는 다음과 같습니다.

  1. sql: 생성된 SQL 문 자체.
  2. parameterMappings: sql 문의 파라미터 매핑 정보. 예를 들어 #{name}은 이름 등에 매핑됩니다.
  3. 매개변수: 수신 매개변수 값의 맵, 키는 매개변수 이름이고 값은 매개변수 값입니다.
    그런 다음 MyBatis는 Configuration의 MappedStatement 개체를 사용하여 이 SQL을 실행하고 MappedStatement에는 BoundSql도 포함됩니다.

그렇다면 MappedStatement와 BoundSql 사이의 관계는 무엇입니까?

MappedStatement와 BoundSql은 밀접한 관계가 있습니다. 간단히 말해서:

  • MappedStatement는 Mapper.xml에서 쿼리 또는 업데이트된 정의 정보를 나타냅니다. 여기에는 SQL 문의 ID, 매개변수 매핑 및 기타 정보가 포함됩니다.
  • BoundSql은 실행할 SQL 문의 세부 정보를 나타냅니다. 여기에는 실제 SQL 문, 매개변수 매핑 및 매개변수 값이 포함됩니다.

Mapper 인터페이스의 메소드를 호출하면 MyBatis는 메소드 서명에 따라 해당 MappedStatement를 찾습니다. 그런 다음 MappedStatement에서 SQL 문을 구문 분석하고 전달한 매개 변수에 따라 실행될 최종 SQL을 생성하며 이 SQL은 BoundSql 개체에 래핑됩니다.

그래서,MappedStatement는 각 호출에서 전달하는 다양한 매개변수에 따라 여러 BoundSql에 해당합니다. 그러나 동일한 호출은 하나의 BoundSql만 생성합니다.

MappedStatement는 MyBatis에 의한 문장의 정적 정의로 이해할 수 있으며 BoundSql은 실행될 때마다 동적으로 생성되는 특정 문장 정보입니다 .

이들의 관계는 다음과 같이 표현할 수 있습니다.

  • MappedStatement는 Mapper.xml의 쿼리 또는 업데이트 정의에 해당합니다.
  • Mapper 인터페이스 메서드가 호출될 때마다 BoundSql이 생성됩니다.
  • 이 BoundSql에는 실행할 최종 SQL 문과 MappedStatement 및 들어오는 매개 변수에 따라 생성된 매개 변수 정보가 포함됩니다.
  • MyBatis는 MappedStatement와 이 BoundSql을 사용하여 SQL을 실행하고 결과를 반환합니다.

동적 SQL 주요 프로세스 분석

여기에 이미지 설명 삽입

SQL노드

SqlNode는 MyBatis에서 조각 SQL을 나타내는 데 사용되는 인터페이스입니다.

일반적으로 MyBaits 프레임워크(SQL로 대체된 포함 태그 포함)를 기반으로 작성하는 Mapper.xml의 각 삽입/업데이트/삭제/선택 태그에 있는 SQL의 각 줄 텍스트는 SqlNode로 추상화됩니다.

여기에 이미지 설명 삽입

각 동적 요소에는 해당 스크립트 클래스가 있습니다. 예를 들어 ifSqlNode에 해당하는 경우 forEarch는 ForEachSqlNode에 해당하는 식입니다. :

  • StaticTextSqlNode: 순수 SQL 문으로 동적 SQL 문을 포함하지 않습니다. 예를 들면 다음과 같습니다.select * from user
  • TextSqlNode: SQL 문은 예를 들어 ${} 자리 표시자를 포함합니다.select * from ${user}
  • IfSqlNode: if/when 하위 태그의 SQL 문입니다.
  • ChooseSqlNode: 하위 태그 선택의 SQL 문입니다.
  • ForEachSqlNode: foreach 하위 태그의 SQL 문입니다.
  • VarDecSqlNode: 바인드 하위 태그의 SQL 문입니다.
  • TrimSqlNode: trim 하위 태그의 SQL 문입니다.
  • WhereSqlNode: where 하위 태그의 SQL 문입니다.
  • SetSqlNode: 집합 하위 태그의 SQL 문입니다.
  • MixedSqlNode: 삽입/업데이트/삭제/선택 태그의 SQL 텍스트가 한 줄 이상인 경우 모든 SqlNode를 함께 어셈블합니다.

여기에 이미지 설명 삽입

SqlNode 인터페이스는 DynamicContext 개체를 통해 각 SqlNode를 완전한 SQL 문으로 어셈블하는 부울 적용(DynamicContext 컨텍스트) 메서드만 정의합니다.

SQL의 각 부분은 SqlNode이며 조건 판단에 따라 포함되거나 제외되며 최종적으로 완전한 SQL 문으로 결합됩니다.

이러한 종류의 SqlNode는 서로 중첩되어 SqlNode 트리를 형성할 수 있습니다. MyBatis는 이 트리와 매개변수 값을 기반으로 최종 SQL 문을 생성합니다.

동적 컨텍스트

DynamicContext는 대나무 막대기의 줄과 같고 SqlNode는 대나무 막대기에 달린 고기 조각입니다. !

SQL 문에 매개 변수가 없는 것처럼 바베큐에 조미료가 없는 이유는 무엇입니까? 매개변수는 DynamicContext의 바인딩 필드에 저장됩니다. getSql() 메서드를 통해 StringJoiner 연결된 SQL 문을 가져옵니다.

여기에 이미지 설명 삽입

  • PARAMETER_OBJECT_KEY: 매개변수 객체의 키를 나타내며 마이바티스는 이 키를 사용하여 컨텍스트에서 컴파일 타임에 전달된 매개변수 객체를 얻습니다.
  • DATABASE_ID_KEY: 데이터베이스 ID의 키를 나타내며 MyBatis는 "MySQL"과 같은 컨텍스트에서 현재 데이터베이스의 ID를 얻기 위해 이 키를 사용합니다.
  • ContextMap: 컨텍스트 Map을 나타내며 MyBatis는 매개변수 개체, 데이터베이스 식별자 등과 같은 현재 컨텍스트와 관련된 다양한 데이터를 이 Map에 저장합니다.
  • StringJoiner: WHERE 절 또는 SET 절에서 SQL 조각을 연결하고 형식화하여 생성된 SQL을 더 깔끔하게 만드는 데 사용됩니다.
  • uniqueNumber: SQL 구문 오류를 방지하기 위해 컬렉션을 순회할 때 결과 집합의 별칭을 생성하는 데 사용되는 정수를 나타냅니다.

소스 코드 분석

StaticTextSqlNode

동적 SQL을 포함하지 않으므로 SQL 문을 연결하기 위해 실제 매개변수에 의존하지 않습니다.

public class StaticTextSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user ");
        DynamicContext dynamicContext = new DynamicContext(configuration, null);
        staticTextSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

여기에 이미지 설명 삽입

StaticTextSqlNode의 소스 코드:

public class StaticTextSqlNode implements SqlNode {
    
    
  private final String text;

  public StaticTextSqlNode(String text) {
    
    
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    
    
    context.appendSql(text);
    return true;
  }

}

StaticTextSqlNode의 소스 코드는 매우 간단합니다. 즉, SQL 문은 DynamicContext의 appendSql() 메서드를 통해 이전 SQL 문 뒤에 연결됩니다.

TextSqlNode

SQL 문에 ${} 자리 표시자가 포함되어 있으므로 자리 표시자를 구문 분석하려면 매개 변수가 필요합니다.

public class TextSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        Map<String, Object> paraMap = new HashMap<>();
        // 把注释放放开并把下面put 方法注解之后会发现解析 ${} 占位符的值为空字符串 
        // Map<String, Object> paraMap = null;
        paraMap.put("user", "haha");
		// paraMap.put("user", "'user'");
        SqlNode textSqlNode = new TextSqlNode("SELECT * FROM ${user}");
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        textSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

여기에 이미지 설명 삽입

TextSqlNode의 소스 코드:

	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 通过 createParse 获取 GenericTokenParser(通用令牌解析器) 对象(主要是解决 ${} 占位符)。
		// 如果发现 ${} 占位符则通过 BindingTokenParser 的 handleToken(String) 方法返回值替换 ${} 占位符
	  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
	  context.appendSql(parser.parse(text));
	  return true;
	}

	@Override
	public String handleToken(String content) {
    
    
	  // 通过 DynamicContext 获取实参
	  Object parameter = context.getBindings().get("_parameter");
	  if (parameter == null) {
    
    
	    context.getBindings().put("value", null);
	  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    
    
	  	// SimpleTypeRegistry 中 SIMPLE_TYPE_SET 包含的类则存在 DynamicContext 参数中
	    context.getBindings().put("value", parameter);
	  }
	  // 通过 OGNL 从实参中获取 ${} 占位符的值
	  Object value = OgnlCache.getValue(content, context.getBindings());
	  String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
	  checkInjection(srtValue);
	  return srtValue;
	}

여기에 이미지 설명 삽입

DynamicContext의 맵에서 매개변수를 가져오는 것을 볼 수 있습니다.

이 GenericTokenParser는 TextSqlNode의 정적 내부 클래스입니다.
여기에 이미지 설명 삽입

IfSqlNode

if/when 하위 태그의 SQL 문은 추상적이고 if 태그의 SQL 문은 if 태그의 테스트 표현식이 참인 경우에만 연결됩니다.

public class IfSqlNodeDemo {
    
    
	public static void main(String[] args) {
    
    
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("user", "user");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(staticTextSqlNode, "user != null");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		ifSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

여기에 이미지 설명 삽입

IfSqlNode의 소스 코드:
여기에 이미지 설명 삽입

	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 通过 OGNL 判断 test 表达式是否成立,表达式里面涉及的属性值通过
		//  DynamicContext 传入的实参获取。如果成立折拼接 SQL 语句
		if (evaluator.evaluateBoolean(test, context.getBindings())) {
    
    
		  contents.apply(context);
		  return true;
		}
		return false;
	}

ChooseSqlNode

choose 하위 태그의 SQL 문은 추상입니다. when 태그의 테스트 표현식이 설정되면 그 안에 있는 SQL 문이 연결되고 그렇지 않으면 else 태그의 SQL 문이 사용됩니다. Java의 if...else if...else 문과 유사하게 하나의 분기 논리만 실행됩니다.

public class ChooseSqlNodeDemo {
    
    
	public static void main(String[] args) {
    
    
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("name", "文海");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE 1 = 1");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"), "name != null");
		SqlNode defaultSqlNode = new StaticTextSqlNode(" AND name = 'wenhai'");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		staticTextSqlNode.apply(dynamicContext);
		// 通过 DynamicContext 拼接 SQL
		ChooseSqlNode chooseSqlNode = new ChooseSqlNode(Collections.singletonList(ifSqlNode), defaultSqlNode);
		chooseSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

ChooseSqlNode의 소스 코드:

	// 通过构造函数传入 when 标签 SQL 抽象和 otherwise 标签的 SQL 抽象
	public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    
    
	  this.ifSqlNodes = ifSqlNodes;
	  this.defaultSqlNode = defaultSqlNode;
	}
	
	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 如果一个分支条件满足就不再执行后面的逻辑
		for (SqlNode sqlNode : ifSqlNodes) {
    
    
		  if (sqlNode.apply(context)) {
    
    
		    return true;
		  }
		}
		// 前面的 when 标签里面的表达式都不满足,并且有兜底的 otherwise 标签则拼接里面的 SQL
		if (defaultSqlNode != null) {
    
    
		  defaultSqlNode.apply(context);
		  return true;
		}
		return false;
	}

ForEachSqlNode

foreach 하위 태그의 SQL 추상화는 항목에 설정된 변수와 태그의 인덱스를 통해 해당 값을 얻을 수 있습니다. index는 배열 및 컬렉션의 인덱스 값이고 Map 유형은 키의 값이고 item은 배열 및 컬렉션의 요소이며 Map 유형은 값의 값입니다.

public class ForeachSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        // 实参对象
        Map<String, Object> paraMap = new HashMap<>();
//        Map<String, String> param = new HashMap<>();
//        param.put("wenhai", "文海");
//        param.put("wenhai2", "文海2");
//        paraMap.put("map", param);
        List<String> list = new ArrayList<>();
        list.add("wenhai");
        list.add("wenhai2");
        paraMap.put("list", list);
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE name in");
        // 通过 DynamicContext 拼接 SQL
        staticTextSqlNode.apply(dynamicContext);
//        String collection = "map";
        String collection = "list";
        String item = "item";
        String index = "index";
        String open = "(";
        String close = ")";
        String separator = ",";
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{index}"), collection, index, item, open, close, separator);

        forEachSqlNode.apply(dynamicContext);
        // 获取 SQL 语句
        String sql = dynamicContext.getSql();
        // 控制台输出 :SELECT * FROM user WHERE name in (  #{__frch_index_0} , #{__frch_index_1} )
        // 同时 DynamicContext 里面的 _parameter 多出以  __frch_#index_n 和 __frch_#item_n 属性值
        // 便于后续通过
        System.out.println(sql);
    }
}

ForEachSqlNode의 소스 코드:

	/**
	 * ForEachSqlNode 构造函数
	 * 
	 * @param configuration			  全局 Configuration 对象
	 * @param contents                foreach 标签里面的 SQL 抽象
	 * @param collectionExpression    foreach 标签里面的 collection 属性值
	 * @param index					  foreach 标签里面的 index 属性值
	 * @param item					  foreach 标签里面的 item 属性值
	 * @param open					  foreach 标签里面的 open 属性值
	 * @param close				      foreach 标签里面的 close 属性值
	 * @param separator               foreach 标签里面的 separator 属性值
	 */
	public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    
    
	   this.evaluator = new ExpressionEvaluator();
	   this.collectionExpression = collectionExpression;
	   this.contents = contents;
	   this.open = open;
	   this.close = close;
	   this.separator = separator;
	   this.index = index;
	   this.item = item;
	   this.configuration = configuration;
	 }


	@Override
	public boolean apply(DynamicContext context) {
    
    
	  // 获取参数列表
	  Map<String, Object> bindings = context.getBindings();
	  // 通过 OGNL 获取 collectionExpression 表达式的值,该值不能为 null,
	  // 只能是 Iterable 实例和数组已经 Map 实例,其他都会报错
	  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
	  if (!iterable.iterator().hasNext()) {
    
    
	    return true;
	  }
	  // 是否是第一次,第一次不用拼接 separator 值
	  boolean first = true;
	  // 如果设置了 open 属性值,则先拼接 open 属性值
	  applyOpen(context);
	  int i = 0;
	  for (Object o : iterable) {
    
    
	    DynamicContext oldContext = context;
	    // 如果是第一次或者是分隔符没有设置则通过 PrefixedContext 包装 DynamicContext 对象
	    // 在 appendSql 方法进行拼接 SQL 时候加上设置的前缀(此处就是 “”)
	    if (first || separator == null) {
    
    
	      context = new PrefixedContext(context, "");
	    } else {
    
    
	      context = new PrefixedContext(context, separator);
	    }
	    // 获取唯一序列号递增用于集合的索引
	    int uniqueNumber = context.getUniqueNumber();
	    // 为 DynamicContext 中的类型为 ContextMap 属性保存 foreach 遍历对应的值
	    // 以 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber 为 key
	    if (o instanceof Map.Entry) {
    
    
	      @SuppressWarnings("unchecked")
	      Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
	      applyIndex(context, mapEntry.getKey(), uniqueNumber);
	      applyItem(context, mapEntry.getValue(), uniqueNumber);
	    } else {
    
    
	      applyIndex(context, i, uniqueNumber);
	      applyItem(context, o, uniqueNumber);
	    }
	    // 通过 FilteredDynamicContext 包装 PrefixedContext 替换 foreach 标签里面
	    // 以 #{} 占位符并且使用正则表达式匹配 item 以及 index 属性值为 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber
	    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
	    if (first) {
    
    
	      first = !((PrefixedContext) context).isPrefixApplied();
	    }
	    context = oldContext;
	    i++;
	  }
	  // 如果 foreach 标签里面的 close 属性设置了则拼接在 SQL 语句后面
	  applyClose(context);
	  context.getBindings().remove(item);
	  context.getBindings().remove(index);
	  return true;
	}

나머지 SqlNode는 분석되지 않고 모두 유사하며 DynamicContext를 래핑하여 효과를 얻습니다.

동적 스크립트 구조

스크립트 간에 내포된 관계가 있습니다. 예를 들어 if 요소는 MixedSqlNode를 포함하고 MixedSqlNode는 1:1 이상의 다른 노드를 포함합니다. 마지막으로 스크립트 구문 트리가 형성됩니다. 아래와 같이 왼쪽의 SQL 요소는 오른쪽의 구문 트리를 형성합니다.노드 하단에는 StaticTextNode 또는 TextNode가 있어야 합니다.

여기에 이미지 설명 삽입

동적 스크립트 실행

SqlNode의 인터페이스는 매우 간단합니다. 적용 방법은 하나뿐입니다. 이 방법의 기능은 현재 스크립트 노드의 논리를 실행하고 결과를 DynamicContext에 적용하는 것입니다.

public interface SqlNode {
  boolean apply(DynamicContext context);
}

예를 들어 IfSqlNode에서 적용을 실행할 때 If 논리가 먼저 계산되고 통과하면 계속해서 자식 노드를 방문합니다. TextNode가 최종적으로 액세스될 때까지 DynamicContext에 SQL 텍스트를 추가합니다. 이와 유사한 재귀 방법을 통해 Context는 모든 노드를 방문하고 최종 자격을 갖춘 SQL 텍스트를 Context에 추가합니다.

//IfSqlNode
public boolean apply(DynamicContext context) {
    
    //计算if表达示
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    
    
    contents.apply(context);
    return true;
  }
  return false;
}
//StaticTextSqlNode
public boolean apply(DynamicContext context) {
    
    
  context.appendSql(text);
  return true;
}

여기에 이미지 설명 삽입
모든 노드를 방문한 후 SQL 문자열이 생성되지만 내부 매개변수가 여전히 #{name=name} 식 형식이므로 직접 실행 가능한 SQL은 아닙니다. 실행 가능한 SQL 및 매개변수 매핑 ParameterMapping을 빌드하려면 SqlSourceBuilder를 사용해야 합니다. 그래야만 BoundSql을 생성할 수 있습니다. 아래 그림은 컨텍스트에서 모든 노드가 실행된 후 BoundSql이 생성되는 것을 보여줍니다.

여기에 이미지 설명 삽입
소스 코드를 보면 동적 SQL에서 BoundSql로 넘어가는 과정에서 중간에 StaticSqlSource 생성이 있었나요? 이 작업을 수행하려는 이유는 무엇이며 XML에서 구문 분석된 SqlNode 집합은 어디에 저장되어 있습니까? 여기에는 새로운 개념의 SqlSource SQL 소스도 있습니다.

SqlSource

SqlNode는 DynamicContext를 통해 SqlNode 간에 SQL을 연결하는데, 실제로 이 기능은 SqlSource를 통해 이루어집니다. SqlSource 인터페이스의 getBoundSql() 메서드를 통해 BoundSql을 가져오고 BoundSql에는 전체 SQL 문, 매개변수 목록 및 실제 매개변수가 포함됩니다. SqlSource는 XML 파일 또는 주석에서 읽은 매핑된 문의 내용을 나타냅니다. 작성하는 SQL은 사용자로부터 받은 입력 매개변수를 기반으로 데이터베이스로 전달됩니다.

SqlSource와 SqlNode 사이의 관계는 무엇입니까?

  • MyBatis에서 SqlSource와 SqlNode는 모두 SQL 문을 캡슐화하는 데 사용되는 클래스입니다.
  • SqlNode는 MyStaticTextSqlNode, IfSqlNode, WhereSqlNode 등이며 각 SqlNode는 SQL 조각을 처리합니다. 이러한 SqlNode를 결합하여 SQL 문의 트리 구조를 형성할 수 있습니다.
  • SqlSource는 SQL 문의 트리 구조를 완전한 SQL 문으로 연결하고 매개 변수 매핑 기능을 제공합니다. SqlSource는 보통 SqlNode로 구성되는데, Mapper.xml 파일에서 SQL 문의 트리 구조를 파싱하여 최종 스플라이싱된 SQL을 JDBC 실행기로 전달한다.
  • 그러므로,SqlNode는 SqlSource를 생성하는 중간 제품입니다. SqlNode는 Mapper.xml 파일을 구문 분석하여 트리 구조의 SQL 문을 형성하고 SqlSource는 이러한 SQL 문을 완전한 SQL로 연결하고 매개 변수 매핑을 수행하고 마지막으로 실행을 위해 JDBC 실행기로 전달합니다.

SqlSource에는 다음 범주가 있습니다.

  • DynamicSqlSource: 动态 SQL${}자리 표시자 에 대한 SQL
  • RawSqlSource: #{}자리 표시자용 SQL
  • ProviderSqlSource: @*Provider주석에 제공된 SQL
  • StaticSqlSource: ?자리 표시자가 있는 SQL 만 포함

Mybatis가 #{}를 처리할 때 sql의 #{}를 ? 숫자로 바꾸고 PreparedStatement의 set 메서드를 호출하여 값을 할당하고
Mybatis 가 를 처리할 때 변수의 값으로 ${}바꿉니다 .${}

여기에 이미지 설명 삽입

정적 SQL 소스

/**
 * {@link StaticSqlSource} 实例里面的 SQL 语句仅包含 ? 占位符。
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class StaticSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        String sql = "SELECT * FROM user WHERE id = #{id}";
        SqlSource staticSqlSource = new StaticSqlSource(configuration, sql);
        BoundSql boundSql = staticSqlSource.getBoundSql(5L);
        System.out.println(boundSql.getSql());
    }
}

아무 처리 없이 위 프로그램의 콘솔 출력을 실행합니다 SELECT * FROM user WHERE id = #{id}.

public class StaticSqlSource implements SqlSource {
    
    
  // SQL 语句
  private final String sql;
  // 参数映射列表
  private final List<ParameterMapping> parameterMappings;
  // 全局 Configuration 对象
  private final Configuration configuration;

  public StaticSqlSource(Configuration configuration, String sql) {
    
    
    this(configuration, sql, null);
  }

  public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
    
    
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 直接构建 BoundSql 对象返回
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

}

StaticSqlSource#getBoundSql 메서드를 보면 BoundSql 객체를 얻을 때 원본 SQL 문이 처리되지 않는다는 것을 알 수 있다.

DynamicSqlSource

/**
 * {@link DynamicSqlSource} 包含动态 SQL 和 ${} 占位符
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class DynamicSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        // 实参对象
        Map<String, Object> paraMap = new HashMap<>();
        List<String> list = new ArrayList<>();
        list.add("wenhai");
        list.add("wenhai2");
        paraMap.put("list", list);
        paraMap.put("id", 5);
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE");
        SqlNode textSqlNode = new TextSqlNode(" id = ${id} AND name IN");
        String collection = "list";
        String item = "item";
        String index = "index";
        String open = "(";
        String close = ")";
        String separator = ",";
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{item}"), collection, index, item, open, close, separator);
        SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(staticTextSqlNode, textSqlNode, forEachSqlNode));
        SqlSource sqlSource = new DynamicSqlSource(configuration, mixedSqlNode);
        BoundSql boundSql = sqlSource.getBoundSql(paraMap);
        System.out.println(boundSql.getSql());
    }
}

위의 프로그램 콘솔 출력 실행SELECT * FROM user WHERE id = 5 AND name IN ( ? , ? )

public class DynamicSqlSource implements SqlSource {
    
    

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    
    
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 构建 DynamicContext 对象来处理 SqlNode
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 通过 SqlSourceBuilder#parse 方法来处理通过 DynamicContext 拼接过的 SQL
    // 主要处理 #{} 占位符替换成 ? 占位符和获取 ParameterMapping 列表
    // 构建 StaticSqlSource 对象
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 设置参数比如 foreach 标签的里面的额外参数等
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

DynamicSqlSouce#getBoundSql() 메서드를 통해 BoundSql 객체를 획득하면 SqlNode가 처리되는데 동적 SQL이고 ${} placeholder를 의미하는 SQL 문이라면 들어오는 실제 매개변수에 따라 splice 및 replace #{} placeholder라면? 교체하고 마지막으로 StaticSqlSource를 통해 BoundSql을 빌드합니다.

RawSql소스

/**
 * {@link RawSqlSource} 不包含动态 SQL 和 ${} 占位符
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class RawSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        SqlNode sqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE id = #{id}");
        SqlSource sqlSource = new RawSqlSource(configuration, sqlNode, Long.class);
        System.out.println(sqlSource.getBoundSql(5L).getSql());
    }
}

위 프로그램의 콘솔 출력을 실행합니다 SELECT * FROM user WHERE id = ?. ${} 자리 표시자에 대해 #{} 자리 표시자가 캐시되거나 SqlNode가 다른 동적 SqlNode로 대체되면 어떻게 됩니까?

public class RawSqlSource implements SqlSource {
    
    
  // 存储构建好的 StaticSqlSource 
  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    
    
  	// 通过 getSql 方法获取 SQL 语句,此时没有传入实参,所以那些动态 SQL 和 ${} 占位符
  	// 无法处理,只能处理 #{} 占位符的 SqlNode
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    
    
  	// 通过 SqlSourceBuilder#parse 方法替换 #{} 占位符为 ? 并构建 #{} 占位符的参数映射列表 
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    
    
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 直接通过 StaticSqlSource#getBoundSql 获取 BoundSql 实例
    return sqlSource.getBoundSql(parameterObject);
  }

}

예제에서 제기된 질문은 소스 코드 분석을 통해 답변할 수 있습니다. ${} 자리 표시자인 경우 처리되지 않으며 동적 SQL이 오류를 보고하거나 처리된 SQL 문이 불완전할 수 있습니다.

ProviderSqlSource

/**
 * {@link ProviderSqlSource} @*Provider 注解提供的 SQL
 *
 * @author wenhai
 * @date 2021/7/21
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */

public class ProviderSqlSourceDemo {
    
    

    public static void main(String[] args) throws NoSuchMethodException {
    
    
        Configuration configuration = new Configuration();
        SelectProvider provider = UserMapper.class.getMethod("select", String.class).getAnnotation(SelectProvider.class);
        SqlSource providerSqlSource = new ProviderSqlSource(configuration, provider, null, null);
        System.out.println(providerSqlSource.getBoundSql("wenhai").getSql());
    }

    public String getSql() {
    
    
        return "SELECT * FROM user WHERE name = #{name}";
    }

    interface UserMapper {
    
    
        @SelectProvider(type = ProviderSqlSourceDemo.class, method = "getSql")
        List<User> select(String name);
    }
}

위의 프로그램 콘솔 출력 실행SELECT * FROM user WHERE name = ?

	@Override
	public BoundSql getBoundSql(Object parameterObject) {
    
    
	  // 通过 @*Provider 注解元信息通过反射调用方法拿到 SQL,
	  // 然后通过 XMLLanguageDriver#createSqlSource 方法解析 SQL 语句
	  // 获取 DynamicSqlSource/RawSqlSource -> StaticSqlSource
	  SqlSource sqlSource = createSqlSource(parameterObject);
	  return sqlSource.getBoundSql(parameterObject);
	}

요약하자면:
SqlNode를 얻기 위해 SQL 소스에 따라 SQL을 구문 분석하고 DynamicSqlSource 또는 RawSqlSource인지 여부에 관계없이 SqlNode에 따라 해당 SqlSource를 얻습니다. DynamicSqlSource가 동적 SQL을 접합하고 실제 매개변수에 따라 ${} 자리 표시자를 처리한 다음 SqlSourceBuilder#parse() 메서드를 통해 StaticSqlSource로 변환하고 RawSqlSource가 인스턴스화될 때 SqlSourceBuilder#parse() 메서드를 통해 StaticSqlSource로 변환되면 실제 매개변수에 의존하지 않으므로 성능이 DynamicSqlSource보다 빠릅니다. ProviderSqlSource는 SQL 문을 파싱한 후 XMLLanguageDriver#createSqlSource() 메소드를 통해 DynamicSqlSource 또는 RawSqlSource를 얻는다.

요약하다

상위 정의에서 각 Sql 매핑(MappedStatement)에는 실행 가능한 Sql(BoundSql)을 얻기 위한 SqlSource가 포함됩니다. SqlSource는 기본 SQL 소스, 동적 SQL 소스 및 타사 소스로 나뉩니다. 관계는 다음과 같습니다.

여기에 이미지 설명 삽입

  • ProviderSqlSource: 세 번째 메서드 SQL 소스, SQL을 가져올 때마다 매개 변수를 기반으로 정적 데이터 소스를 동적으로 생성한 다음 BoundSql을 생성합니다.
  • DynamicSqlSource: 동적 SQL 소스에는 SQL 스크립트가 포함되어 있으며 SQL을 얻을 때마다 매개 변수 및 스크립트를 기반으로 BoundSql을 동적으로 생성하고 생성합니다.
  • RawSqlSource: 동적 요소, 기본 텍스트를 포함하지 않는 SQL입니다. 그러나이 SQL은 직접 실행할 수 없으며 BoundSql로 변환해야 합니다.
  • StaticSqlSource: BoundSql을 직접 생성할 수 있는 실행 가능한 SQL 및 매개 변수 매핑을 포함합니다. 처음 세 데이터 원본은 먼저 StaticSqlSource를 만든 다음 BoundSql을 만들어야 합니다.

SqlSource 구문 분석 프로세스

SqlSource는 XML 구문 분석을 기반으로 합니다. 구문 분석의 맨 아래 계층은 Dom4j를 사용하여 XML을 하위 노드로 구문 분석하는 것입니다. XMLScriptBuilder를 통해 이러한 하위 노드를 순회한 후 해당 Sql 소스가 최종적으로 생성됩니다. 분석 프로세스는 다음과 같습니다.

여기에 이미지 설명 삽입

이는 모든 노드에 대한 재귀적 액세스임을 그림에서 알 수 있으며, 텍스트 노드인 경우 TextNode 또는 StaticSqlNode가 직접 생성됩니다. 그렇지 않으면 IfSqlNode와 같은 동적 스크립트 노드가 생성됩니다. 여기서 동적 노드의 각 유형은 해당 프로세서(NodeHandler)로 생성됩니다. 생성된 후에는 하위 노드를 계속 방문하여 재귀를 계속할 수 있습니다. 물론 자식 노드에 의해 생성된 SqNode는 현재 생성된 요소의 자식 노드로도 존재하게 됩니다.

여기에 이미지 설명 삽입

추천

출처blog.csdn.net/zyb18507175502/article/details/131122767