web-dev-qa-db-fra.com

Récupérer un objet de entityframework sans UN champ

J'utilise le framework d'entité pour me connecter à la base de données. J'ai un petit problème:

J'ai une table qui a une colonne varbinary (MAX) (avec filestream).

J'utilise la requête SQL pour gérer la partie "Data", mais EF pour le reste (métadonnées du fichier).

J'ai un code qui doit obtenir tous les fichiers id, nom de fichier, guid, date de modification, ... d'un fichier. Cela n'a pas du tout besoin du champ "Données".

Existe-t-il un moyen de récupérer une liste sans que cette colonne soit remplie?

Quelque chose comme

context.Files.Where(f=>f.xyz).Exclude(f=>f.Data).ToList();

??

Je sais que je peux créer des objets anonymes, mais je dois transmettre le résultat à une méthode, donc pas de méthodes anonymes. Et je ne veux pas mettre cela dans une liste de type anonyme, puis créer une liste de mon type non anonyme (Fichier).

Le but est d'éviter cela:

using(RsSolutionsEntities context = new RsSolutionsEntities())
{
    var file = context.Files
        .Where(f => f.Id == idFile)
        .Select(f => new {
            f.Id, f.MimeType, f.Size, f.FileName, f.DataType,
            f.DateModification, f.FileId
        }).FirstOrDefault();

    return new File() {
        DataType = file.DataType, DateModification = file.DateModification,
        FileId = file.FileId, FileName = file.FileName, Id = file.Id,
        MimeType = file.MimeType, Size = file.Size
    };
}

(J'utilise ici le type anonyme car sinon vous obtiendrez une NotSupportedException: l'entité ou le type complexe 'ProjectName.File' ne peut pas être construit dans une requête LINQ to Entities.)

(par exemple, ce code lève l'exception précédente:

File file2 = context.Files.Where(f => f.Id == idFile)
  .Select(f => new File() {Id = f.Id, DataType = f.DataType}).FirstOrDefault();

et "Fichier" est le type que j'obtiens avec une context.Files.ToList(). C'est la bonne classe:

using File = MyProjectNamespace.Common.Data.DataModel.File;

Le fichier est une classe connue de mon contexte de données EF:

public ObjectSet<File> Files
{
    get { return _files  ?? (_files = CreateObjectSet<File>("Files")); }
}
private ObjectSet<File> _files;
50
J4N

Existe-t-il un moyen de récupérer une liste sans que cette colonne soit remplie?

Pas sans projection que vous souhaitez éviter. Si la colonne est mappée, elle fait naturellement partie de votre entité. L'entité sans cette colonne n'est pas complète - il s'agit d'un ensemble de données différent = projection.

J'utilise ici le type anonyme, sinon vous obtiendrez une exception NotSupportedException: l'entité ou le type complexe "ProjectName.File" ne peut pas être construit dans une requête LINQ to Entities.

Comme le dit l'exception, vous ne pouvez pas projeter vers une entité mappée. J'ai mentionné la raison ci-dessus - la projection crée un ensemble de données différent et EF n'aime pas les "entités partielles".

Erreur 16 Erreur 3023: Problème de mappage des fragments à partir de la ligne 2717: Fichiers de colonne. Les données de la table Les fichiers doivent être mappés: il n'a pas de valeur par défaut et n'est pas annulable.

Il ne suffit pas de supprimer une propriété du concepteur. Vous devez également ouvrir EDMX en XML et supprimer la colonne de SSDL, ce qui rendra votre modèle très fragile (chaque mise à jour de la base de données remettra votre colonne en place). Si vous ne souhaitez pas mapper la colonne, vous devez utiliser la vue de base de données sans la colonne et mapper la vue à la place de la table, mais vous ne pourrez pas insérer de données.

Pour contourner tous vos problèmes, utilisez fractionnement de table et séparez la colonne binaire problématique d'une autre entité avec une relation 1: 1 avec votre entité principale File.

19
Ladislav Mrnka

Je ferais quelque chose comme ça:

var result = from thing in dbContext.Things
             select new Thing {
                 PropertyA = thing.PropertyA,
                 Another = thing.Another
                 // and so on, skipping the VarBinary(MAX) property
             };

Thing est votre entité qu'EF sait matérialiser. L'instruction SQL résultante ne doit pas inclure la grande colonne dans son jeu de résultats, car elle n'est pas nécessaire dans la requête.

EDIT : De vos modifications, vous obtenez l'erreur NotSupportedException: l'entité ou le type complexe 'ProjectName.File' ne peut pas être construit dans une requête LINQ to Entities. parce que vous n'avez pas mappé cette classe en tant qu'entité. Vous impossible incluez des objets dans les requêtes LINQ to Entities que EF ne connaît pas et attendez-vous à ce qu'il génère des instructions SQL appropriées.

Vous pouvez mapper un autre type qui exclut la colonne VarBinary(MAX) dans sa définition ou utiliser le code ci-dessus.

11
Yuck

tu peux le faire:

var files = dbContext.Database.SqlQuery<File>("select FileId, DataType, MimeType from Files");

ou ca:

var files = objectContext.ExecuteStoreQuery<File>("select FileId, DataType, MimeType from Files");

en fonction de votre version d'EF

8
Jeremy Danyow

J'ai eu cette exigence parce que j'ai une entité Document qui a un champ Content avec le contenu du fichier, c'est-à-dire environ 100 Mo, et j'ai une fonction de recherche que je voulais retourner reste des colonnes.

J'ai choisi d'utiliser la projection:

IQueryable<Document> results = dbContext.Documents.Include(o => o.UploadedBy).Select(o => new {
    Content = (string)null,
    ContentType = o.ContentType,
    DocumentTypeId = o.DocumentTypeId,
    FileName = o.FileName,
    Id = o.Id,
    // etc. even with related entities here like:
    UploadedBy = o.UploadedBy
});

Ensuite, mon contrôleur WebApi transmet cet objet results à une fonction de pagination commune, qui applique un .Skip, .Take et un .ToList.

Cela signifie que lorsque la requête est exécutée, elle n'accède pas à la colonne Content, de sorte que les données de 100 Mo ne sont pas touchées et que la requête est aussi rapide que vous le souhaitez/attendez.

Ensuite, je la rejette dans ma classe DTO, qui dans ce cas est à peu près exactement la même que la classe d'entité, donc ce n'est peut-être pas une étape que vous devez implémenter, mais elle suit mon modèle de codage WebApi typique, donc:

var dtos = paginated.Select(o => new DocumentDTO
{
    Content = o.Content,
    ContentType = o.ContentType,
    DocumentTypeId = o.DocumentTypeId,
    FileName = o.FileName,
    Id = o.Id,
    UploadedBy = o.UploadedBy == null ? null : ModelFactory.Create(o.UploadedBy)
});

Ensuite, je renvoie la liste DTO:

return Ok(dtos);

Il utilise donc la projection, qui pourrait ne pas correspondre aux exigences de l'affiche originale, mais si vous utilisez des classes DTO, vous effectuez quand même la conversion. Vous pouvez tout aussi facilement effectuer les opérations suivantes pour les renvoyer que vos entités réelles:

var dtos = paginated.Select(o => new Document
{
    Content = o.Content,
    ContentType = o.ContentType,
    DocumentTypeId = o.DocumentTypeId,
    //...

Juste quelques étapes supplémentaires, mais cela fonctionne bien pour moi.

6
Sean

Pour EF Core 2, j'ai implémenté une solution comme celle-ci:

var files = context.Files.AsNoTracking()
                         .IgnoreProperty(f => f.Report)
                         .ToList();

L'idée de base est de tourner par exemple cette requête:

SELECT [f].[Id], [f].[Report], [f].[CreationDate]
FROM [File] AS [f]

en cela:

SELECT [f].[Id], '' as [Report], [f].[CreationDate]
FROM [File] AS [f]

vous pouvez voir le code source complet ici: https://github.com/aspnet/EntityFrameworkCore/issues/1387#issuecomment-495630292

2
HamedH

J'utilise ici le type anonyme, sinon vous obtiendrez une exception NotSupportedException: l'entité ou le type complexe "ProjectName.File" ne peut pas être construit dans une requête LINQ to Entities.

var file = context.Files
        .Where(f => f.Id == idFile)
        .FirstOrDefault() // You need to exeucte the query if you want to reuse the type
        .Select(f => new {
            f.Id, f.MimeType, f.Size, f.FileName, f.DataType,
            f.DateModification, f.FileId
        }).FirstOrDefault();

Et ce n'est pas non plus une mauvaise pratique de dénormaliser davantage la table, c'est-à-dire une avec des métadonnées et une avec une charge utile pour éviter la projection. La projection fonctionnerait, le seul problème est que vous devez modifier chaque fois qu'une nouvelle colonne est ajoutée au tableau.

1
skjagini

J'aimerais partager mes tentatives pour contourner ce problème au cas où quelqu'un d'autre se trouverait dans la même situation.

J'ai commencé avec ce que Jeremy Danyow a suggéré, ce qui est pour moi l'option la moins douloureuse.

// You need to include all fields in the query, just make null the ones you don't want.
var results = context.Database.SqlQuery<myEntity>("SELECT Field1, Field2, Field3, HugeField4 = NULL, Field5 FROM TableName");

Dans mon cas, j'avais besoin d'un objet de résultat IQueryable<> Alors j'ai ajouté AsQueryable() à la fin. Bien sûr, cela m'a permis d'ajouter des appels à .Where, .Take Et aux autres commandes que nous connaissons tous, et ils ont bien fonctionné. Mais il y a une mise en garde:

Le code normal (essentiellement context.myEntity.AsQueryable()) a renvoyé un System.Data.Entity.DbSet<Data.DataModel.myEntity>, Tandis que cette approche a renvoyé System.Linq.EnumerableQuery<Data.DataModel.myEntity>.

Apparemment, cela signifie que ma requête personnalisée est exécutée "telle quelle" dès que nécessaire et que le filtrage que j'ai ajouté plus tard est effectué par la suite et non dans la base de données.

J'ai donc essayé d'imiter l'objet d'Entity Framework en utilisant la requête exacte qu'EF crée, même avec ces alias [Extent1], Mais cela n'a pas fonctionné. Lors de l'analyse de l'objet résultant, sa requête s'est terminée comme

FROM [dbo].[TableName] AS [Extent1].Where(c => ...

au lieu de l'attendu

FROM [dbo].[TableName] AS [Extent1] WHERE ([Extent1]...

Quoi qu'il en soit, cela fonctionne, et tant que la table n'est pas énorme, cette méthode sera assez rapide. Sinon, vous n'avez pas d'autre option que d'ajouter manuellement les conditions en concaténant des chaînes, comme le SQL dynamique classique. Un exemple très basique au cas où vous ne savez pas de quoi je parle:

string query = "SELECT Field1, Field2, Field3, HugeField4 = NULL, Field5 FROM TableName";
if (parameterId.HasValue)
    query += " WHERE Field1 = " + parameterId.Value.ToString();
var results = context.Database.SqlQuery<myEntity>(query);

Si votre méthode a parfois besoin de ce champ, vous pouvez ajouter un paramètre bool puis faire quelque chose comme ceci:

IQueryable<myEntity> results;
if (excludeBigData)
    results = context.Database.SqlQuery<myEntity>("SELECT Field1, Field2, Field3, HugeField4 = NULL, Field5 FROM TableName").AsQueryable();
else
    results = context.myEntity.AsQueryable();

Si quelqu'un parvient à faire fonctionner correctement les extensions Linq comme s'il s'agissait de l'objet EF d'origine, veuillez commenter afin que je puisse mettre à jour la réponse.

0
Andrew

J'ai essayé ceci:

Dans le diagramme edmx (EF 6), j'ai cliqué sur la colonne que je voulais cacher à EF et sur leurs propriétés, vous pouvez définir leur getter et setter sur private. De cette façon, pour moi, cela fonctionne.

Je renvoie certaines données qui incluent une référence utilisateur, donc je voulais masquer le champ Mot de passe même s'il est crypté et salé, je ne le voulais tout simplement pas sur mon json, et je ne voulais pas faire un:

Select(col => new {}) 

parce que c'est pénible à créer et à maintenir, surtout pour les grandes tables avec beaucoup de relations.

L'inconvénient de cette méthode est que si vous régénérez votre modèle, vous devrez à nouveau modifier son getter et son setter.

0
mikesoft