5. Dynamic SQL AST, OGNL & Parameter Binding
MyBatis 3's Dynamic SQL engine (org.apache.ibatis.scripting.*, 25 source files) uses the Interpreter and Composite design patterns to turn XML tags (<if>, <choose>, <when>, <otherwise>, <where>, <set>, <trim>, <foreach>, <bind>) into an executable SqlNode syntax tree.
Source Code
from drawlib.canvas import save, setup
from drawlib.diagrams.class_diagram import ClassDiagram, ClassNode
from drawlib.fonts import FontMonoSpace, FontRoboto
from drawlib.styles import Colors, Styles
setup(width=144, height=76, dpi=150)
cd = ClassDiagram(
width=144.0,
height=76.0,
node_style=Styles.Neutral.patch(
shape_fill_color=Colors.White,
shape_line_color=Colors.Gray6,
text_color=Colors.Dark,
text_font=FontMonoSpace.ROBOTO_MONO_REGULAR,
text_size=9.2,
),
header_style=Styles.PrimaryNeutral.patch(
shape_line_color=Colors.Gray6,
text_color=Colors.Dark,
text_font=FontRoboto.ROBOTO_BOLD,
text_size=9.8,
),
edge_style=Styles.Dark.patch(line_width=1.4),
edge_text_style=Styles.DarkBold.patch(text_font=FontRoboto.ROBOTO_BOLD, text_size=9.2),
)
hero_hdr = Styles.PrimaryFlat.patch(text_color=Colors.White, text_font=FontRoboto.ROBOTO_BOLD, text_size=9.8)
sec_hdr = Styles.SecondaryNeutral.patch(shape_line_color=Colors.Gray6, text_color=Colors.Dark, text_font=FontRoboto.ROBOTO_BOLD, text_size=9.8)
n_src = cd.add(
ClassNode("SqlSource", stereotype="interface", width=42.0, header_style=hero_hdr)
.add_method("getBoundSql", "parameterObject", "BoundSql"),
xy=(28.0, 60.0),
)
n_node = cd.add(
ClassNode("SqlNode [Composite AST]", stereotype="interface", width=44.0, header_style=hero_hdr)
.add_method("apply", "DynamicContext", "boolean"),
xy=(104.0, 60.0),
)
n_raw = cd.add(
ClassNode("RawSqlSource [Fast Path]", width=42.0, header_style=sec_hdr)
.add_attribute("sqlSource", "StaticSqlSource")
.add_method("compiled once at startup", "", ""),
xy=(26.0, 34.0),
)
n_dyn = cd.add(
ClassNode("DynamicSqlSource", width=42.0, header_style=sec_hdr)
.add_attribute("rootSqlNode", "SqlNode")
.add_method("getBoundSql (per call)", "param", "BoundSql"),
xy=(72.0, 34.0),
)
n_if = cd.add(
ClassNode("IfSqlNode & ChooseSqlNode", width=42.0)
.add_attribute("evaluator", "ExpressionEvaluator (OGNL)")
.add_attribute("test", "String"),
xy=(46.0, 11.0),
)
n_for = cd.add(
ClassNode("ForEachSqlNode & TrimSqlNode", width=46.0)
.add_attribute("collectionExpression", "String")
.add_method("apply (binds __frch_item_0)", "ctx", "bool"),
xy=(106.0, 11.0),
)
cd.connect(n_raw, n_src, "realization", start_side="top", end_side="bottom")
cd.connect(n_dyn, n_src, "realization", start_side="top", end_side="bottom")
cd.connect(n_dyn, n_node, "association", directed=True, start_side="right", end_side="bottom")
cd.connect(n_if, n_node, "realization", start_side="top", end_side="bottom")
cd.connect(n_for, n_node, "realization", start_side="top", end_side="bottom")
cd.draw(xy=(0.0, 0.0))
save()
RawSqlSource (Compile-Time Fast Path) vs. DynamicSqlSource (Runtime AST Evaluation)
When XMLScriptBuilder.parseScriptNode() (src/main/java/org/apache/ibatis/scripting/xmltags/XMLScriptBuilder.java:53-68) inspects a mapper statement:
- Static SQL Fast Path (
RawSqlSource->StaticSqlSource):- If the SQL contains no dynamic XML tags (
<if>,<where>,<foreach>, etc.) and no${}unescaped string interpolations (TextSqlNode.isDynamic() == false),XMLScriptBuilderreturns aRawSqlSource. RawSqlSourcerunsSqlSourceBuilder.parse()once at bootstrap time, replacing every#{prop,jdbcType=VARCHAR}token with a?placeholder and pre-computing the immutableList<ParameterMapping>. At runtime,getBoundSql()requires zero token parsing!
- If the SQL contains no dynamic XML tags (
- Dynamic SQL Runtime Path (
DynamicSqlSource):- If the statement contains any dynamic XML tag or
${}token,XMLScriptBuilderreturns aDynamicSqlSourceholding the rootMixedSqlNode. - On each query execution,
DynamicSqlSource.getBoundSql(parameterObject)creates aDynamicContext, walksrootSqlNode.apply(context)(evaluating<if test="...">predicates viaOgnlCache.getValue(expression, bindings)), and then passescontext.getSql()throughSqlSourceBuilder.parse()to convert#{}tokens into?and produce theBoundSql.
- If the statement contains any dynamic XML tag or
Security & Performance Distinction: #{} vs. ${}
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Colors, Styles
setup(width=144, height=62, dpi=150)
white_card = Styles.Neutral.patch(shape_fill_color=Colors.White)
left_panel = Panel(
"#{} Token Pipeline (Safe JDBC Binding)",
"SqlSourceBuilder -> DefaultParameterHandler",
style=Styles.Neutral,
header_style=Styles.PrimaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=10.8),
description_style=Styles.Muted.patch(text_size=9.5),
icon=phosphor.shield_check,
icon_style=Styles.PrimaryBold,
header_variant="filled",
header_height=8.0,
)
left_panel.draw((4.0, 4.5), width=66.5, height=53.0, align="bottom_left")
l_xy, l_w, l_h = left_panel.get_content_box((4.0, 4.5), width=66.5, height=53.0, padding=(2.6, 2.4), align="bottom_left")
safe_cards = CardList(
style=white_card,
text_style=Styles.DarkBold.patch(text_size=10.0),
description_style=Styles.Muted.patch(text_size=9.4),
badge_shape="rounded_rect",
badge_style=Styles.PrimaryFlat,
badge_text_style=Styles.WhiteBold.patch(text_size=9.5),
direction="vertical",
spacing=2.0,
)
safe_cards.add(
"1. GenericTokenParser ('#{', '}')",
"Replaces #{id} with '?' & builds ParameterMapping",
badge_text="S1",
)
safe_cards.add(
"2. DefaultParameterHandler.setParameters",
"Resolves value from additionalParameters / MetaObject",
badge_text="S2",
)
safe_cards.add(
"3. TypeHandler.setParameter(ps, i, val)",
"Binds typed JDBC parameter; immune to SQLi (CWE-89)",
badge_text="S3",
)
safe_cards.draw(l_xy, width=l_w, height=l_h, align="bottom_left")
right_panel = Panel(
"${} Token Pipeline (Raw String Interpolation)",
"TextSqlNode.BindingTokenParser (Static Metadata Only)",
style=Styles.Neutral,
header_style=Styles.SecondaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=10.8),
description_style=Styles.Muted.patch(text_size=9.5),
icon=phosphor.warning_circle,
icon_style=Styles.WarningBold,
header_variant="filled",
header_height=8.0,
)
right_panel.draw((73.5, 4.5), width=66.5, height=53.0, align="bottom_left")
r_xy, r_w, r_h = right_panel.get_content_box((73.5, 4.5), width=66.5, height=53.0, padding=(2.6, 2.4), align="bottom_left")
raw_cards = CardList(
style=white_card,
text_style=Styles.DarkBold.patch(text_size=10.0),
description_style=Styles.Muted.patch(text_size=9.4),
badge_shape="rounded_rect",
badge_style=Styles.WarningFlat,
badge_text_style=Styles.WhiteBold.patch(text_size=9.5),
direction="vertical",
spacing=2.0,
)
raw_cards.add(
"1. TextSqlNode ('${', '}')",
"Evaluates OGNL & appends raw string into SQL text",
badge_text="R1",
)
raw_cards.add(
"2. Forces DynamicSqlSource Path",
"Disables RawSqlSource compile-time pre-parsing",
badge_text="R2",
)
raw_cards.add(
"3. Restrict to Allowlisted Identifiers",
"Never pass untrusted user input into ${orderBy}",
badge_text="R3",
)
raw_cards.draw(r_xy, width=r_w, height=r_h, align="bottom_left")
save()
Inside DefaultParameterHandler.setParameters(PreparedStatement ps)
Located at src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java:89-160 (the highest-complexity method in the codebase at cognitive=129), setParameters() iterates over boundSql.getParameterMappings() and resolves each propertyName in strict precedence order:
boundSql.hasAdditionalParameter(propertyName): Synthetic parameters bound by<foreach>(__frch_item_0,__frch_item_1) or<bind name="pattern" value="'%' + name + '%'"/>.parameterObject == null: Value isnull.typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()): Primitive/scalar parameter (Long,String,UUID, etc.) is bound directly.MetaObject.getValue(propertyName): Domain JavaBean, Record, orParamMapproperty traversal viaConfiguration.newMetaObject(parameterObject).