Skip to content

增加支持在ResultMap中使用连续属性访问对结果集对象中的嵌套属性赋值; #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/SmartSql.Test.Unit/Deserializer/EntityDeserializerTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using SmartSql.Test.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -75,5 +76,37 @@ public async Task QueryAsync()
});
Assert.NotNull(list);
}
// Unit test for nested property mapping functionality
// 嵌套属性映射功能的单元测试
[Fact]
public void NestedPropertyMappingTest()
{
// Execute query with nested property mapping
// 执行包含嵌套属性映射的查询
var list = SqlMapper.Query<NestedEntity>(new RequestContext
{
Scope = nameof(AllPrimitive),
SqlId = "QueryNestedPropertyResult", // SQL statement ID (SQL语句ID)
Request = new { Taken = 10000 } // Query parameters (查询参数)
});

// Verify result validity
// 验证结果有效性
Assert.NotNull(list); // Result list should not be null (结果列表不应为空)

// Validate nested properties existence
// 验证嵌套属性存在性
Assert.NotNull(list.First().NestedProp1); // Level 1 nesting (第一层嵌套)
Assert.NotNull(list.First().NestedProp1.NestedProp2); // Level 2 nesting (第二层嵌套)
Assert.NotNull(list.First().NestedProp1.NestedProp2.NestedProp3); // Level 3 nesting (第三层嵌套)

// Test Purpose:
// Verifies ORM's ability to handle multi-level nested object mapping
// 验证ORM处理多级嵌套对象映射的能力
// Key Validations:
// 1. Correct parsing of nested property paths (正确解析嵌套属性路径)
// 2. Proper object initialization at each level (各级对象正确初始化)
// 3. Maintains data integrity through mapping (通过映射保持数据完整性)
}
}
}
21 changes: 21 additions & 0 deletions src/SmartSql.Test.Unit/Maps/AllPrimitive.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
//*******************************-->

<SmartSqlMap Scope="AllPrimitive" xmlns="http://SmartSql.net/schemas/SmartSqlMap.xsd">
<ResultMaps>
<ResultMap Id="NestedPropertyResultMap">
<Result Column="Id" Property="Id"/>
<Result Column="String" Property="NestedProp1.NestedProp2.NestedProp3"/>
</ResultMap>
</ResultMaps>
<MultipleResultMaps>
<MultipleResultMap Id="GetByPage">
<Result Property="List"/>
Expand Down Expand Up @@ -513,6 +519,21 @@
</IsNotEmpty>
</Statement>

<!--获取数据列 返回的ResultMap包含连续属性访问-->
<Statement Id="QueryNestedPropertyResult" ResultMap="NestedPropertyResultMap">
SELECT
T.* From T_AllPrimitive T
<Include RefId="QueryParams"/>
<Switch Prepend="Order By" Property="OrderBy">
<Default>
T.Id Desc
</Default>
</Switch>
<IsNotEmpty Prepend="limit" Property="Taken">
?Taken
</IsNotEmpty>
</Statement>


<!--获取分页数据-->
<Statement Id="QueryByPage">
Expand Down
17 changes: 17 additions & 0 deletions src/SmartSql.Test/Entities/NestedEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

namespace SmartSql.Test.Entities
{
public class NestedEntity
{
public virtual long Id { get; set; }
public virtual NestedProperty1 NestedProp1 { get; set; }
}
public class NestedProperty1
{
public virtual NestedProperty2 NestedProp2 { get; set; }
}
public class NestedProperty2
{
public virtual string NestedProp3 { get; set; }
}
}
129 changes: 117 additions & 12 deletions src/SmartSql/Deserializer/EntityDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,70 @@ private Delegate CreateDeserialize<TResult>(ExecutionContext executionContext)
ilGen.Call(DataType.Method.IsDBNull);
ilGen.IfTrueS(isDbNullLabel);
}
// Handle property chain access logic (处理属性链访问逻辑)
if (propertyHolder.IsChain)
{
// Load root object instance (加载根对象实例)
ilGen.LoadLocalVar(0); // Stack: [currentObj]

// Traverse all properties except the last one in the chain (遍历属性链中除最后属性外的所有属性)
foreach (var prop in propertyHolder.PropertyChain.Take(propertyHolder.PropertyChain.Count - 1))
{
// Define null-check label (定义空值检查标签)
var notNullLabel = ilGen.DefineLabel();

// ==== Start null-check logic ==== (开始空值检查逻辑)

// 1. Preserve current instance reference (保留当前实例引用)
ilGen.Dup(); // Stack: [currentObj, currentObj]

// 2. Get child property value (获取子属性值)
ilGen.Call(prop.GetMethod); // Stack: [childObj, currentObj]

// 3. Check if child object is null (检查子对象是否为空)
ilGen.IfTrueS(notNullLabel); // Stack: [currentObj] (consumes childObj)

// ==== Null branch ==== (空值分支)

// 1. Preserve parent instance again (再次保留父实例)
ilGen.Dup(); // Stack: [currentObj, currentObj]

// 2. Create new child instance (创建新的子实例)
ilGen.New(prop.PropertyType.GetConstructor(Type.EmptyTypes)); // Stack: [newChildObj, currentObj, currentObj]

// 3. Assign new instance to parent property (将新实例赋值给父属性)
ilGen.Call(prop.SetMethod); // Stack: [currentObj] (consumes currentObj and newChildObj)

// ==== Non-null branch ==== (非空值分支)
ilGen.MarkLabel(notNullLabel); // Stack: [currentObj]

// Get child object (either existing or newly created) (获取子对象:已存在的或新创建的)
ilGen.Call(prop.GetMethod); // Stack: [childObj]

// Now childObj becomes the new currentObj for next iteration (此时childObj成为下一轮迭代的currentObj)
}

// ==== Set final property value ==== (设置最终属性值)

// 1. Load property value onto stack (加载属性值到栈顶)
LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType, propertyHolder.TypeHandler);

// 2. Call final set method (调用最终set方法)
ilGen.Call(propertyHolder.SetMethod); // Stack: [] (consumes childObj and value)
}
// Handle single property access (处理单属性访问)
else
{
// 1. Load root instance (加载根实例)
ilGen.LoadLocalVar(0); // Stack: [currentObj]

// 2. Load property value (加载属性值)
LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType, propertyHolder.TypeHandler);

// 3. Directly set property (直接设置属性)
ilGen.Call(propertyHolder.SetMethod); // Stack: [] (consumes currentObj and value)
}

ilGen.LoadLocalVar(0);
LoadPropertyValue(ilGen, executionContext, propertyType, columnDescriptor.FieldType,
propertyHolder.TypeHandler);
ilGen.Call(propertyHolder.SetMethod);
if (ignoreDbNull)
{
ilGen.MarkLabel(isDbNullLabel);
Expand Down Expand Up @@ -278,21 +337,46 @@ public static void ThrowDeserializeException(Exception ex, Object result, int co

private static bool ResolveProperty<TResult>(ResultMap resultMap, Type resultType,
ColumnDescriptor columnDescriptor
, out PropertyHolder propertyHolder)
, out IPropertyHolder propertyHolder)
{
propertyHolder = null;
if (resultMap?.Properties != null)
{
if (resultMap.Properties.TryGetValue(columnDescriptor.ColumnName, out var resultProperty))
{
var property = resultType.GetProperty(resultProperty.Name) ??
throw new SmartSqlException($"ResultMap:[{resultMap.Id}], can not find property:[{resultProperty.Name}] in class:[{resultType.Name}]");
propertyHolder = new PropertyHolder
// Handle nested property path (处理嵌套属性路径)
if (resultProperty.Name.Contains('.'))
{
Property = property,
TypeHandler = resultProperty.TypeHandler
};
return true;
// Parse property chain (e.g. "User.Address.City") and create chain holder
// 解析属性链(例如"User.Address.City")并创建链式属性容器
propertyHolder = new PropertyChainHolder(
ParsePropertyChain(
resultMapId: resultMap.Id, // 当前ResultMap ID
rootType: resultType, // 根对象类型
propertyPath: resultProperty.Name // 属性路径字符串
),
resultProperty.TypeHandler // 类型处理器
);
return true; // Successfully resolved property chain (成功解析属性链)
}
// Handle single property (处理单属性)
else
{
// Get property info from result type (从结果类型获取属性信息)
var property = resultType.GetProperty(resultProperty.Name)
?? throw new SmartSqlException(
$"ResultMap:[{resultMap.Id}], can not find property:[{resultProperty.Name}] in class:[{resultType.Name}]"
// 错误格式:结果映射:[ID], 在类[类名]中找不到属性[属性名]
);

// Create standard property holder (创建标准属性容器)
propertyHolder = new PropertyHolder
{
Property = property, // 目标属性
TypeHandler = resultProperty.TypeHandler // 类型处理器
};
return true; // Successfully resolved single property (成功解析单属性)
}
}
}

Expand Down Expand Up @@ -321,6 +405,27 @@ ColumnDescriptor columnDescriptor
return false;
}

private static List<PropertyInfo> ParsePropertyChain(string resultMapId, Type rootType, string propertyPath)
{
var propertyNames = propertyPath.Split('.');
var chain = new List<PropertyInfo>();
Type currentType = rootType;

foreach (var name in propertyNames)
{
var property = currentType.GetProperty(name);
if (property == null)
{
throw new SmartSqlException($"ResultMap:[{resultMapId}], Cannot find property:[{name}] in type:[{currentType.Name}] for path:[{propertyPath}]");
}

chain.Add(property);
currentType = property.PropertyType;
}

return chain;
}

private void LoadPropertyValue(ILGenerator ilGen, ExecutionContext executionContext,
Type propertyType, Type fieldType, String typeHandler)
{
Expand Down
28 changes: 28 additions & 0 deletions src/SmartSql/Reflection/IPropertyHolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace SmartSql.Reflection
{
internal interface IPropertyHolder
{
PropertyInfo Property { get; set; }
String TypeHandler { get; set; }

Type PropertyType { get; }

bool CanWrite { get; }

MethodInfo SetMethod { get; }

/// <summary>
/// 是否为属性链(如 User.Address.City)
/// </summary>
bool IsChain { get; }

/// <summary>
/// 属性链中的中间属性(仅当 IsChain = true 时有效)
/// </summary>
IReadOnlyList<PropertyInfo> PropertyChain { get; }
}
}
82 changes: 82 additions & 0 deletions src/SmartSql/Reflection/PropertyChainHolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace SmartSql.Reflection
{
/// <summary>
/// Represents a chain of nested properties (e.g. User.Address.City)
/// ��ʾǶ�������������� User.Address.City��
/// </summary>
public class PropertyChainHolder : IPropertyHolder
{
private readonly PropertyInfo property;

/// <summary>
/// Gets the final property in the chain (��ȡ�����������һ������)
/// Note: Setter is explicitly disabled for immutability (Setter ����ʽ�����Ա�֤���ɱ���)
/// </summary>
public PropertyInfo Property { get => property; set => throw new NotSupportedException(); }

/// <summary>
/// Type handler for property value conversion (����ֵ����ת��������)
/// </summary>
public string TypeHandler { get; set; }

/// <summary>
/// Property type of the final property (�������Ե�����)
/// </summary>
public Type PropertyType => Property.PropertyType;

/// <summary>
/// Indicates if all properties in the chain are writable (��ʶ�����������������Ƿ��д)
/// Pre-calculated during initialization for performance (��ʼ��ʱԤ�������Ż�����)
/// </summary>
public bool CanWrite { get; }

/// <summary>
/// Setter method of the final property (�������Ե����÷���)
/// Essential for reflection-based value assignment (���ڷ��丳ֵ�Ĺؼ�����)
/// </summary>
public MethodInfo SetMethod => Property.SetMethod;

/// <summary>
/// Explicit marker for chain property type (��ȷ��ʶ������ʽ��������)
/// Always returns true for this implementation (�ڱ�ʵ���к㷵�� true)
/// </summary>
public bool IsChain => true;

/// <summary>
/// Immutable list of property chain elements (���ɱ��������Ԫ�ؼ���)
/// Stored as read-only collection for thread safety (�洢Ϊֻ�������Ա�֤�̰߳�ȫ)
/// </summary>
public IReadOnlyList<PropertyInfo> PropertyChain { get; }

/// <summary>
/// Constructs a property chain holder (���캯��)
/// </summary>
/// <param name="propertyChain">
/// Ordered list of properties in the chain (�����������б�)
/// Must contain at least one element (�����������һ��Ԫ��)
/// </param>
/// <param name="typeHandler">
/// Type conversion handler for the property (��������ת��������)
/// </param>
public PropertyChainHolder(List<PropertyInfo> propertyChain, string typeHandler)
{
// Capture final property (������������)
property = propertyChain.Last();

// Create defensive copy as read-only (���������Ը�����Ϊֻ������)
PropertyChain = propertyChain.AsReadOnly();

// Pre-calculate writability status (Ԥ�����д״̬)
CanWrite = PropertyChain.All(property => property.CanWrite);

// Store type handler (�洢���ʹ�����)
TypeHandler = typeHandler;
}
}
}
8 changes: 7 additions & 1 deletion src/SmartSql/Reflection/PropertyHolder.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace SmartSql.Reflection
{
public class PropertyHolder
public class PropertyHolder : IPropertyHolder
{
public PropertyInfo Property { get; set; }
public String TypeHandler { get; set; }
Expand All @@ -13,5 +14,10 @@ public class PropertyHolder
public bool CanWrite => Property.CanWrite;

public MethodInfo SetMethod => Property.SetMethod;

public bool IsChain => false;

public IReadOnlyList<PropertyInfo> PropertyChain => throw new NotSupportedException();

}
}