可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Consider the following example LINQ to entity query
from history in entities.foreignuserhistory
select new { history.displayname, login=history.username, history.foreignuserid }
ToTraceString()
return string looks like:
SELECT "Extent1"."foreignuserid" AS "foreignuserid",
"Extent1"."displayname" AS "displayname",
"Extent1"."username" AS "username"
FROM "integration"."foreignuserhistory" AS "Extent1"
The problem for me is that columns come in different order from query and do not take aliases like login
in the example. Where does Entity Framework store mapping information for anonymous types?
Background: I'm going to develop insert with select operation using LINQ to entity for mass operations.
Update:
Insert with select is not that hard except for an unknown column to property mapping algorithm. One can get table and column names for destination ObjectSet
using metadata, build INSERT INTO tableName (column_name1, …)
sql statement string and then append some ObjectQuery.ToTraceString
SELECT statement. Then create a DbCommand
with resulting text using ((EntityConnection)ObjectContext.Connection).StoreConnection
and fill command’s parameters from ObjectQuery
. So the problem is to find matching column order in inserted and selected records.
回答1:
Here’s my solution all the way down of privates and internals. It travels with reflection into cached query plan which will exist after ToTraceString
call or query execution to get what is called _columnMap
. Column map contains ScalarColumnMap
objects going in the order of anonymous object’s properties and pointing to the corresponding column position with ColumnPos
property.
using System;
using System.Data.Objects;
using System.Reflection;
static class EFQueryUtils
{
public static int[] GetPropertyPositions(ObjectQuery query)
{
// get private ObjectQueryState ObjectQuery._state;
// of actual type internal class
// System.Data.Objects.ELinq.ELinqQueryState
object queryState = GetProperty(query, "QueryState");
AssertNonNullAndOfType(queryState, "System.Data.Objects.ELinq.ELinqQueryState");
// get protected ObjectQueryExecutionPlan ObjectQueryState._cachedPlan;
// of actual type internal sealed class
// System.Data.Objects.Internal.ObjectQueryExecutionPlan
object plan = GetField(queryState, "_cachedPlan");
AssertNonNullAndOfType(plan, "System.Data.Objects.Internal.ObjectQueryExecutionPlan");
// get internal readonly DbCommandDefinition ObjectQueryExecutionPlan.CommandDefinition;
// of actual type internal sealed class
// System.Data.EntityClient.EntityCommandDefinition
object commandDefinition = GetField(plan, "CommandDefinition");
AssertNonNullAndOfType(commandDefinition, "System.Data.EntityClient.EntityCommandDefinition");
// get private readonly IColumnMapGenerator EntityCommandDefinition._columnMapGenerator;
// of actual type private sealed class
// System.Data.EntityClient.EntityCommandDefinition.ConstantColumnMapGenerator
object columnMapGenerator = GetField(commandDefinition, "_columnMapGenerator");
AssertNonNullAndOfType(columnMapGenerator, "System.Data.EntityClient.EntityCommandDefinition+ConstantColumnMapGenerator");
// get private readonly ColumnMap ConstantColumnMapGenerator._columnMap;
// of actual type internal class
// System.Data.Query.InternalTrees.SimpleCollectionColumnMap
object columnMap = GetField(columnMapGenerator, "_columnMap");
AssertNonNullAndOfType(columnMap, "System.Data.Query.InternalTrees.SimpleCollectionColumnMap");
// get internal ColumnMap CollectionColumnMap.Element;
// of actual type internal class
// System.Data.Query.InternalTrees.RecordColumnMap
object columnMapElement = GetProperty(columnMap, "Element");
AssertNonNullAndOfType(columnMapElement, "System.Data.Query.InternalTrees.RecordColumnMap");
// get internal ColumnMap[] StructuredColumnMap.Properties;
// array of internal abstract class
// System.Data.Query.InternalTrees.ColumnMap
Array columnMapProperties = GetProperty(columnMapElement, "Properties") as Array;
AssertNonNullAndOfType(columnMapProperties, "System.Data.Query.InternalTrees.ColumnMap[]");
int n = columnMapProperties.Length;
int[] propertyPositions = new int[n];
for (int i = 0; i < n; ++i)
{
// get value at index i in array
// of actual type internal class
// System.Data.Query.InternalTrees.ScalarColumnMap
object column = columnMapProperties.GetValue(i);
AssertNonNullAndOfType(column, "System.Data.Query.InternalTrees.ScalarColumnMap");
//string colName = (string)GetProp(column, "Name");
// can be used for more advanced bingings
// get internal int ScalarColumnMap.ColumnPos;
object columnPositionOfAProperty = GetProperty(column, "ColumnPos");
AssertNonNullAndOfType(columnPositionOfAProperty, "System.Int32");
propertyPositions[i] = (int)columnPositionOfAProperty;
}
return propertyPositions;
}
static object GetProperty(object obj, string propName)
{
PropertyInfo prop = obj.GetType().GetProperty(propName, BindingFlags.NonPublic | BindingFlags.Instance);
if (prop == null) throw EFChangedException();
return prop.GetValue(obj, new object[0]);
}
static object GetField(object obj, string fieldName)
{
FieldInfo field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
if (field == null) throw EFChangedException();
return field.GetValue(obj);
}
static void AssertNonNullAndOfType(object obj, string fullName)
{
if (obj == null) throw EFChangedException();
string typeFullName = obj.GetType().FullName;
if (typeFullName != fullName) throw EFChangedException();
}
static InvalidOperationException EFChangedException()
{
return new InvalidOperationException("Entity Framework internals has changed, please review and fix reflection code");
}
}
I think some assertions can be relaxed to check not the exact type but base type containing necessary property.
Is there a solution without reflection?
回答2:
How the columns are aliased in the query shouldn't matter, and neither should their order. Entity Framework handles populating a new instance of your anonymous type with each result, and that's where you get the alias like login
.
As a side note, I think Entity Framework may not work quite how you think. You can't do a select/insert in a single operation like you can using a normal SQL query. Entity Framework will execute your select, return back the results, use those results to create new instances of your entities (or in your case, an anonymous type), and you would then have to use each result to create a new instance of your target type, adding each one to your entity/object context, and finally call save changes on your entity/object context. This will cause an individual insert statement to be executed for each new entity that you've added.
If you want to do it all in a single operation without instantiating a new entity for every record, you'll need to either use a stored procedure that you map in your context, or else execute an in-line SQL query using ObjectContext.ExecuteStoreCommand
UPDATE: Based on your responses, what you're really getting into is closer to meta-programming that relies on your entity model more so than actually using entity framework. I don't know what version of EF you're using (EF 4.0? 4.1 w/ code first and DbContext?), but I've had a lot of success using the C# POCO template with EF 4.0 (the POCO template is a download from the online visual studio gallery). It uses a T4 template to generate POCO classes from the .edmx data model. In your T4 template, you could add methods to your context that would essentially call ExecuteStoreCommand
, but the difference would be you can generate the query that gets executed based on your data model. That way any time your data model changes, your query would stay in sync with the changes.
回答3:
Updated the reflection on this for EF 4.4 (5-RC)
full post at http://imaginarydevelopment.blogspot.com/2012/06/compose-complex-inserts-from-select.html
using this functionality/logic for doing a bulk insert from a select with some parameters provided
int Insert<T>(IQueryable query,IQueryable<T> targetSet)
{
var oQuery=(ObjectQuery)this.QueryProvider.CreateQuery(query.Expression);
var sql=oQuery.ToTraceString();
var propertyPositions = GetPropertyPositions(oQuery);
var targetSql=((ObjectQuery)targetSet).ToTraceString();
var queryParams=oQuery.Parameters.ToArray();
System.Diagnostics.Debug.Assert(targetSql.StartsWith("SELECT"));
var queryProperties=query.ElementType.GetProperties();
var selectParams=sql.Substring(0,sql.IndexOf("FROM "));
var selectAliases=Regex.Matches(selectParams,@"\sAS \[([a-zA-Z0-9_]+)\]").Cast<Match>().Select(m=>m.Groups[1].Value).ToArray();
var from=targetSql.Substring(targetSql.LastIndexOf("FROM [")+("FROM [".Length-1));
var fromAlias=from.Substring(from.LastIndexOf("AS ")+"AS ".Length);
var target=targetSql.Substring(0,targetSql.LastIndexOf("FROM ["));
target=target.Replace("SELECT","INSERT INTO "+from+" (")+")";
target=target.Replace(fromAlias+".",string.Empty);
target=Regex.Replace(target,@"\sAS \[[a-zA-z0-9]+\]",string.Empty);
var insertParams=target.Substring(target.IndexOf('('));
target = target.Substring(0, target.IndexOf('('));
var names=Regex.Matches(insertParams,@"\[([a-zA-Z0-9]+)\]");
var remaining=names.Cast<Match>().Select(m=>m.Groups[1].Value).Where(m=>queryProperties.Select(qp=>qp.Name).Contains(m)).ToArray(); //scrape out items that the anonymous select doesn't include a name/value for
//selectAliases[propertyPositions[10]]
//remaining[10]
var insertParamsOrdered = remaining.Select((s, i) => new { Position = propertyPositions[i], s })
.OrderBy(o => o.Position).Select(x => x.s).ToArray();
var insertParamsDelimited = insertParamsOrdered.Aggregate((s1, s2) => s1 + "," + s2);
var commandText = target + "(" + insertParamsDelimited + ")" + sql;
var result=this.ExecuteStoreCommand(commandText,queryParams.Select(qp=>new System.Data.SqlClient.SqlParameter{ ParameterName=qp.Name, Value=qp.Value}).ToArray());
return result;
}