.net - que - Obtenga el nombre de la tabla de la base de datos de Entity Framework MetaData
mvc entity framework español (18)
Estoy tratando de encontrar la forma de obtener el nombre de la tabla SQL subyacente para un tipo de entidad dado. He experimentado con las consultas de MetadataWorkspace y aunque puedo obtener mucha información del objeto o el espacio de almacenamiento, parece que no puedo encontrar la forma de mapear entre los dos.
Entonces, supongamos que tengo un tipo en el modelo de objetos llamado Búsqueda: ¿cómo puedo encontrar el nombre de la tabla (wws_lookups) en la base de datos?
Puedo consultar todos los objetos EntityType para CSpace y SSpace y puedo ver ambos listados correctamente, pero no puedo entender cómo obtener SSpace desde CSpace.
¿Hay alguna manera de hacer esto?
Alex tiene razón: esta es una limitación triste en la API de metadatos. Tengo que cargar el MSL como un documento XML y hacer búsquedas de entidades de S-Space mientras proceso mi modelo de C-espacio.
Aquí hay otra forma de encontrar el nombre de la tabla. Es un poco extraño pero funciona. VB:
For Each Table In northwind.MetadataWorkspace.GetItemCollection(New System.Data.Metadata.Edm.DataSpace)
''adds table name to a list of strings all table names in EF have the project namespace in front of it.''
If Table.ToString.Contains("namespace of project") then
''using substring to remove project namespace from the table name.''
TableNames.Add(Table.ToString.Substring("length of namespace name"))
End If
Next
Aquí hay una versión suponiendo que tiene contexto y tiene una entidad seleccionada en la memoria para la que necesita encontrar el nombre real de la tabla.
public static class ObjectContextExtentions { public static string TableNameFor(this ObjectContext context, ObjectStateEntry entry) { var generic = context.GetType().GetProperties().ToList().First(p => p.Name == entry.EntityKey.EntitySetName); var objectset = generic.GetValue(context, null); var method = objectset.GetType().GetMethod("ToTraceString"); var sql = (String)method.Invoke(objectset, null); var match = Regex.Match(sql, @"FROM/s+/[dbo/]/./[(?<TableName>[^/]]+)/]", RegexOptions.Multiline); if (match.Success) { return match.Groups["TableName"].Value; } throw new ArgumentException("Unable to find Table name."); } }
Copiando mi respuesta a otra pregunta aquí.
Si alguien sigue mirando, así es como lo hice. Este es un método de extensión para DBContext que toma un tipo y devuelve los nombres de las columnas físicas y sus propiedades.
Esto utiliza el contexto del objeto para obtener la lista de columnas físicas, luego utiliza la propiedad de metadatos "NombrePreferido" para asignar cada columna a su propiedad.
Como utiliza el contexto del objeto, inicia una conexión a la base de datos, por lo que la primera ejecución será lenta dependiendo de la complejidad del contexto.
public static IDictionary<String, PropertyInfo> GetTableColumns(this DbContext ctx, Type entityType)
{
ObjectContext octx = (ctx as IObjectContextAdapter).ObjectContext;
EntityType storageEntityType = octx.MetadataWorkspace.GetItems(DataSpace.SSpace)
.Where(x => x.BuiltInTypeKind == BuiltInTypeKind.EntityType).OfType<EntityType>()
.Single(x => x.Name == entityType.Name);
var columnNames = storageEntityType.Properties.ToDictionary(x => x.Name,
y => y.MetadataProperties.FirstOrDefault(x => x.Name == "PreferredName")?.Value as string ?? y.Name);
return storageEntityType.Properties.Select((elm, index) =>
new {elm.Name, Property = entityType.GetProperty(columnNames[elm.Name])})
.ToDictionary(x => x.Name, x => x.Property);
}
Para usarlo, simplemente crea una clase estática auxiliar y agrega la función anterior; entonces es tan simple como llamar
var tabCols = context.GetTableColumns(typeof(EntityType));
EF 6.1, código primero:
public static string GetTableName<T>(this DbContext context) where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.GetTableName(typeof(T));
}
public static string GetTableName(this DbContext context, Type t)
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.GetTableName(t);
}
private static readonly Dictionary<Type,string> TableNames = new Dictionary<Type, string>();
public static string GetTableName(this ObjectContext context, Type t)
{
string result;
if (!TableNames.TryGetValue(t, out result))
{
lock (TableNames)
{
if (!TableNames.TryGetValue(t, out result))
{
string entityName = t.Name;
ReadOnlyCollection<EntityContainerMapping> storageMetadata = context.MetadataWorkspace.GetItems<EntityContainerMapping>(DataSpace.CSSpace);
foreach (EntityContainerMapping ecm in storageMetadata)
{
EntitySet entitySet;
if (ecm.StoreEntityContainer.TryGetEntitySetByName(entityName, true, out entitySet))
{
if (String.IsNullOrEmpty(entitySet.Schema))
{
result = entitySet.Table;
break;
}
//we must recognize if we are under SQL Server Compact version, which does not support multiple schemas
//SQL server compact does not support schemas, entity framework sets entitySet.Schema set to "dbo", anyway
//the System.Data.Entity.Infrastructure.TableExistenceChecker.GetTableName() returns only table name
//schema is (not) added by the overrides of the method AnyModelTableExistsInDatabase
//the SqlCeTableExistenceChecker has the knowledge that there is no metadata schema needed
//the SqlTableExistenceChecker has the knowledge that there is metadata with schema, which should be added to the table names
var entityConnection = (System.Data.Entity.Core.EntityClient.EntityConnection) context.Connection;
DbConnection storeConnection = entityConnection.StoreConnection;
if (storeConnection != null && "SqlCeConnection".Equals(storeConnection.GetType().Name, StringComparison.OrdinalIgnoreCase))
{
result = entitySet.Table;
break;
}
result = entitySet.Schema + "." + entitySet.Table;
break;
}
}
TableNames.Add(t,result);
}
}
}
return result;
}
En realidad, he pasado por el mismo problema y he producido un fragmento de código abstracto que le da dos Dictionary<string,List<string>>
($ table_name, $ columns_name_list). El primero tiene lista de bases de datos + lista de nombres de columnas, el segundo tiene entidades EF locales + propiedades
Por supuesto, puede agregar más controles en función del tipo de datos, por cierto, que lo obligaría a escribir un código increíblemente complicado.
P & L
PD: Perdón por el estilo comprimido, soy un fanático de la lambda
using (EFModelContext efmc = new EFModelContext("appConfigConnectionName"))
{
string schemaName = "dbo";
string sql = @"select o.name + ''.'' + c.name
from sys.all_objects o
inner join sys.schemas s on s.schema_id = o.schema_id
inner join sys.all_columns c on c.object_id = o.object_id
where Rtrim(Ltrim(o.type)) in (''U'') and s.name = @p0";
Dictionary<string, List<string>> dbTableColumns = new Dictionary<string, List<string>>();
efmc.Database.SqlQuery<string>(sql, schemaName).Select(tc =>
{
string[] splitted = System.Text.RegularExpressions.Regex.Split(tc, "[.]");
return new { TableName = splitted[0], ColumnName = splitted[1] };
}).GroupBy(k => k.TableName, k => k.ColumnName).ToList().ForEach(ig => dbTableColumns.Add(ig.Key, ig.ToList()));
Dictionary<string, List<string>> efTableColumns = new Dictionary<string, List<string>>();
efTableColumns = ((IObjectContextAdapter)uc).ObjectContext.MetadataWorkspace
.GetItems(DataSpace.SSpace).OfType<EntityType>()
.ToDictionary( eft => eft.MetadataProperties
.First(mp => mp.Name == "TableName").Value.ToString(),
eft => eft.Properties.Select(p => p.Name).ToList());
}
Esto es lo que pude encontrar usando LINQ to XML. El código también obtiene las asignaciones para los nombres de las columnas.
var d = XDocument.Load("MyModel.edmx");
XNamespace n = "http://schemas.microsoft.com/ado/2008/09/mapping/cs";
var l = (from etm in d.Descendants()
where etm.Name == n + "EntityTypeMapping"
let s = etm.Attribute("TypeName").Value
select new
{
Name = s.Remove(0, s.IndexOf(".") + 1).Replace(")", ""),
Table = etm.Element(n + "MappingFragment").Attribute("StoreEntitySet").Value,
Properties = (from sp in etm.Descendants(n + "ScalarProperty")
select new
{
Name = sp.Attribute("Name").Value,
Column = sp.Attribute("ColumnName").Value
}).ToArray()
}).ToArray();
Hay una manera de borrar datos usando EF sin tener que cargarlo primero. Lo describí en un poco más detenido en: http://nigelfindlater.blogspot.com/2010/04/how-to-delete-objects-in-ef4-without.html
El truco es convertir el IQueriable en un ObjectQuery y usar el método ToTraceString. Luego edite la cadena sql resultante. Funciona, pero debe tener cuidado porque está pasando por alto los mecanismos que EF tiene en su lugar para mantener las dependencias y las limitaciones. Pero por razones de rendimiento, creo que está bien hacer esto ...
que te diviertas...
Nigel ...
private string GetClause<TEntity>(IQueryable<TEntity> clause) where TEntity : class
{
string snippet = "FROM [dbo].[";
string sql = ((ObjectQuery<TEntity>)clause).ToTraceString();
string sqlFirstPart = sql.Substring(sql.IndexOf(snippet));
sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", "");
sqlFirstPart = sqlFirstPart.Replace("[Extent1].", "");
return sqlFirstPart;
}
public void DeleteAll<TEntity>(IQueryable<TEntity> clause) where TEntity : class
{
string sqlClause = GetClause<TEntity>(clause);
this.context.ExecuteStoreCommand(string.Format(CultureInfo.InvariantCulture, "DELETE {0}", sqlClause));
}
No, lamentablemente es imposible usar las API de metadatos para obtener el nombre de tabla para una entidad determinada.
Esto se debe a que los metadatos de asignación no son públicos, por lo que no hay forma de pasar de C-Space a S-Space utilizando las API de EF.
Si realmente necesita hacer esto, siempre puede construir el mapa usted mismo analizando el MSL. Esto no es para corazones débiles, pero debería ser posible, a menos que esté usando QueryViews (que son increíblemente raros), en cuyo punto es imposible para todos los propósitos (tendría que analizar ESQL ... argh! )
Alex James
Microsoft.
Para EF6, mezclar / comprimir el código de otras respuestas aquí y alrededor (VB, lo siento):
Public Function getDBTableName(data As myDataModel, ByVal entity As Object) As String
Dim context = CType(data, IObjectContextAdapter).ObjectContext
Dim sName As String = entity.GetType.BaseType.Name ''use BaseType to avoid proxy names''
Dim map = context.MetadataWorkspace.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).FirstOrDefault
Return (From esmap In map.EntitySetMappings
Select esmap.EntityTypeMappings.First(
Function(etm)
etm.EntityType.Name = sName
).Fragments.First.StoreEntitySet.Name).FirstOrDefault
''TODO: use less .first everywhere but filter the correct ones''
End Function
Funciona para db-first.
Relativamente fácil de entender siguiendo un archivo .edmx.
Puede intentar la extensión MappingAPI: https://efmappingapi.codeplex.com/
Es realmente fácil de usar
context.Db<YourEntityType>().TableName
Si está haciendo codefirst en EF6, puede agregar algo como lo siguiente a su clase dbcontext.
public string GetTableName(Type entityType)
{
var sql = Set(entityType).ToString();
var regex = new Regex(@"FROM /[dbo/]/./[(?<table>.*)/] AS");
var match = regex.Match(sql);
return match.Groups["table"].Value;
}
Si está utilizando la plantilla T4 para las clases POCO, puede obtenerla alterando la Plantilla T4. Ver fragmento:
<#
////////////////////////////////////////////////////////////////////////////////
region.Begin("Custom Properties");
string xPath = "//*[@TypeName=''" + entity.FullName + "'']";
XmlDocument doc = new XmlDocument();
doc.Load(inputFile);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("edmx", "http://schemas.microsoft.com/ado/2008/10/edmx");
XmlNode item;
XmlElement root = doc.DocumentElement;
item = root.SelectSingleNode(xPath);
#>
//<#= xPath #>
//<#= entity.FullName #>
//<#= (item == null).ToString() #>
<# if (item != null) #>
// Table Name from database
public string TableName { get { return "<#= item.ChildNodes[0].Attributes["StoreEntitySet"].Value #>"; } }
<#
region.End();
////////////////////////////////////////////////////////////////////////////////
Una mejor manera es usar StoreItemCollection de Metadata. Este tipo ya ha proporcionado un ejemplo de uso: obtener tablas y relaciones
Una posible solución alternativa (no excelente, pero tampoco lo son las alternativas ...):
var sql = Context.EntitySetName.ToTraceString();
... luego analizar el SQL, que debería ser bastante simple.
Usando EF5 y un poquito de reflexión, algo como lo siguiente debería hacer el truco:
using System;
using System.Collections;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;
namespace EFHelpers {
public class EFMetadataMappingHelper {
public static string GetTableName(MetadataWorkspace metadata, DbEntityEntry entry) {
var entityType = entry.Entity.GetType();
var objectType = getObjectType(metadata, entityType);
var conceptualSet = getConceptualSet(metadata, objectType);
var storeSet = getStoreSet(metadata, conceptualSet);
var tableName = findTableName(storeSet);
return tableName;
}
private static EntitySet getStoreSet(MetadataWorkspace metadata, EntitySetBase entitySet) {
var csSpace = metadata.GetItems(DataSpace.CSSpace).Single();
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var entitySetMaps = (ICollection)csSpace.GetType().GetProperty("EntitySetMaps", flags).GetValue(csSpace, null);
object mapping = null;
foreach (var map in entitySetMaps) {
var set = map.GetType().GetProperty("Set", flags).GetValue(map, null);
if (entitySet == set) {
mapping = map;
break;
}
}
var m_typeMappings = ((ICollection)mapping.GetType().BaseType.GetField("m_typeMappings", flags).GetValue(mapping)).OfType<object>().Single();
var m_fragments = ((ICollection)m_typeMappings.GetType().BaseType.GetField("m_fragments", flags).GetValue(m_typeMappings)).OfType<object>().Single();
var storeSet = (EntitySet) m_fragments.GetType().GetProperty("TableSet", flags).GetValue(m_fragments, null);
return storeSet;
}
private static string findTableName(EntitySet storeSet) {
string tableName = null;
MetadataProperty tableProperty;
storeSet.MetadataProperties.TryGetValue("Table", true, out tableProperty);
if (tableProperty == null || tableProperty.Value == null)
storeSet.MetadataProperties.TryGetValue("http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator:Table", true, out tableProperty);
if (tableProperty != null)
tableName = tableProperty.Value as string;
if (tableName == null)
tableName = storeSet.Name;
return tableName;
}
private static EntityType getObjectType(MetadataWorkspace metadata, Type entityType) {
var objectItemCollection = (ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace);
var edmEntityType = metadata
.GetItems<EntityType>(DataSpace.OSpace)
.First(e => objectItemCollection.GetClrType(e) == entityType);
return edmEntityType;
}
private static EntitySetBase getConceptualSet(MetadataWorkspace metadata, EntityType entityType) {
var entitySetBase = metadata
.GetItems<EntityContainer>(DataSpace.CSpace)
.SelectMany(a => a.BaseEntitySets)
.Where(s => s.ElementType.Name == entityType.Name)
.FirstOrDefault();
return entitySetBase;
}
}
}
Llamarlo es así:
public string GetTableName(DbContext db, DbEntityEntry entry) {
var metadata = ((IObjectContextAdapter)db).ObjectContext.MetadataWorkspace;
return EFMetadataMappingHelper.GetTableName(metadata, entry);
}
EDITAR Esta respuesta ahora obsoleta debido a la nueva función en EF 6.1: mapeo entre tipos de tablas . Ve allí primero!
Tuve un problema con las otras respuestas porque tengo un tipo derivado. Obtuve este método (dentro de mi clase de contexto) para trabajar - Tengo solo una capa de herencia en mi modelo en este momento
private readonly static Dictionary<Type, EntitySetBase> _mappingCache
= new Dictionary<Type, EntitySetBase>();
private ObjectContext _ObjectContext
{
get { return (this as IObjectContextAdapter).ObjectContext; }
}
private EntitySetBase GetEntitySet(Type type)
{
if (_mappingCache.ContainsKey(type))
return _mappingCache[type];
type = GetObjectType(type);
string baseTypeName = type.BaseType.Name;
string typeName = type.Name;
ObjectContext octx = _ObjectContext;
var es = octx.MetadataWorkspace
.GetItemCollection(DataSpace.SSpace)
.GetItems<EntityContainer>()
.SelectMany(c => c.BaseEntitySets
.Where(e => e.Name == typeName
|| e.Name == baseTypeName))
.FirstOrDefault();
if (es == null)
throw new ArgumentException("Entity type not found in GetEntitySet", typeName);
// Put es in cache.
_mappingCache.Add(type, es);
return es;
}
internal String GetTableName(Type type)
{
EntitySetBase es = GetEntitySet(type);
//if you are using EF6
return String.Format("[{0}].[{1}]", es.Schema, es.Table);
//if you have a version prior to EF6
//return string.Format( "[{0}].[{1}]",
// es.MetadataProperties["Schema"].Value,
// es.MetadataProperties["Table"].Value );
}
internal Type GetObjectType(Type type)
{
return System.Data.Entity.Core.Objects.ObjectContext.GetObjectType(type);
}
NB: hay planes para mejorar la Metadata API y, si esto no está obteniendo lo que queremos, entonces podemos ver el Primer Código de EF Mapeo entre Tipos y Tablas
Uso el enfoque de Nigel (extrayendo el nombre de la tabla de .ToTraceString()
) pero con algunas modificaciones, porque su código no funcionará si la tabla no está en el esquema predeterminado de SQL Server ( dbo.{table-name}
).
DbContext
métodos de extensión para objetos DbContext
y ObjectContext
:
public static class ContextExtensions
{
public static string GetTableName<T>(this DbContext context) where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter) context).ObjectContext;
return objectContext.GetTableName<T>();
}
public static string GetTableName<T>(this ObjectContext context) where T : class
{
string sql = context.CreateObjectSet<T>().ToTraceString();
Regex regex = new Regex(@"FROM/s+(?<table>.+)/s+AS");
Match match = regex.Match(sql);
string table = match.Groups["table"].Value;
return table;
}
}
Más detalles aquí:
Marco de la entidad: obtener el nombre de la tabla asignada de una entidad