Usando polimorfismo com MongoDB e C#
Avalie esse Tutorial
Em comparação com relational database management systems (RDBMS), o esquema flexível do MongoDB é um grande avanço no tratamento de dados orientados a objetos. Essas estruturas geralmente fazem uso de polimorfismo, onde as classes base comuns contêm os campos compartilhados que estão disponíveis para todas as classes na hierarquia; classes derivadas adicionam os campos que são relevantes apenas para os objetos específicos. Um exemplo poderia ser ter vários tipos de veículos, como carros e motocicletas, que possuem alguns campos em comum, mas cada tipo também adiciona alguns campos que só fazem sentido se usados para um tipo:
Para o RDBMS, armazenar uma hierarquia de objetos é um desafio. Uma maneira é armazenar os dados em uma tabela que contém todos os campos de todas as classes, embora para cada linha seja necessário apenas um subconjunto de campos. Outra abordagem é criar uma tabela para a classe base que contém os campos compartilhados e adicionar uma tabela para cada classe derivada que armazena as colunas do tipo específico e faz referência à tabela base. Nenhuma dessas abordagens é ideal em termos de armazenamento e quando se trata de consultar os dados.
No entanto, com o esquema flexível do MongoDB, pode-se armazenar facilmente documentos na mesma coleção que compartilham apenas alguns, mas não todos os campos. Este artigo mostra como o driverdoMongoDB C# facilita o uso disso para armazenar hierarquias de classes de uma forma muito natural.
Exemplos de casos de uso incluem o armazenamento de metadados para vários tipos de documentos, por exemplo, ofertas, faturas ou outros documentos relacionados a parceiros de negócios em uma collection. Os campos comuns podem ser o título do documento, um resumo, a data, uma incorporação vetorial e a referência ao parceiro de negócios, enquanto uma fatura adicionaria campos para os itens e totais da linha, mas não adicionaria os campos para um relatório de projeto.
Outro caso de uso possível é servir uma visão geral e uma visualização detalhada da mesma collection. Veremos mais de perto como implementar isso no resumo deste artigo.
Ao acessar uma collection do C#, usamos um objeto que implementa a interface
IMongoCollection<T>
. Este objeto pode ser criado assim:1 var vehiclesColl = db.CreateCollection<Vehicle>("vehicles");
Ao serializar ou deserializar documentos, o parâmetro de tipo
T
e o tipo real do objeto fornecem ao driver C# do MongoDB uma dica sobre como mapear a representação BSON para uma classe C# e vice-versa. Se apenas documentos do mesmo tipo residirem na coleção, o driver usará o mapa de classe do tipo.No entanto, para poder lidar com as hierarquias de classes corretamente, o driver precisa de mais informações. É aqui que entra o discriminador de tipo. Ao armazenar um documento de um tipo derivado na coleção, o driver adiciona um campo chamado
_t
ao documento que contém o nome da classe, por exemplo:1 await vehiclesColl.InsertOneAsync(new Car());
leva à seguinte estrutura de documento:
1 { 2 "_id": ObjectId("660d7d43e042f8f6f2726f6a"), 3 "_t": "Car", 4 // ... fields for vehicle 5 // ... fields specific to car 6 }
Ao desserializar o documento, o valor do campo
_t
é utilizado para identificar o tipo do objeto que deve ser criado.Embora isso funcione imediatamente sem configuração específica, é recomendável oferecer suporte ao driver especificando a hierarquia de classes explicitamente usando o atributo
BsonKnownTypes
, se você estiver usando o mapeamento declarativo:1 [ ]2 public abstract class Vehicle 3 { 4 // ... 5 }
Se você configurar os mapas de classe imperativamente, basta adicionar um mapa de classe para cada tipo na hierarquia para obter o mesmo efeito.
Por padrão, somente o nome da classe é usado como valor para o discriminador de tipo. especialmente se a hierarquia abranger vários níveis e você desejar executar query de qualquer nível da hierarquia, deverá armazenar a hierarquia como uma array no discriminador de tipo usando o atributo
BsonDiscriminator
:1 [ ]2 [ ]3 public abstract class Vehicle 4 { 5 // ... 6 }
Isso aplica uma convenção discriminatória diferente aos documentos e armazena a hierarquia como uma matriz:
1 { 2 "_id": ObjectId("660d81e5825f1c064024a591"), 3 "_t": [ 4 "Vehicle", 5 "Car" 6 ], 7 // ... 8 }
Para obter mais detalhes sobre como configurar os mapas de classes para objetos polimórficos, consulte a documentação do driver.
Ao ler objetos de uma coleção, o driver C# do MongoDB usa o discriminador de tipo para identificar o tipo correspondente e criar um objeto C# da classe correspondente. A seguinte query pode gerar
Car
Motorcycle
objetos e :1 var vehiclesColl = db.GetCollection<Vehicle>("vehicles"); 2 var vehicles = (await vehiclesColl.FindAsync(FilterDefinition<Vehicle>.Empty)) 3 .ToEnumerable();
Se você estiver interessado apenas em documentos de um tipo específico, poderá criar outra instância de
IMongoCollection<T>
que retorne apenas estes:1 var carsColl = vehiclesColl.OfType<Car>(); 2 var cars = (await carsColl.FindAsync(FilterDefinition<Car>.Empty)) 3 .ToEnumerable();
Esta nova instância de coleção respeita o discriminador de tipo correspondente sempre que uma operação é executada. A declaração a seguir remove apenas
Car
documentos da coleção, mas mantém os documentosMotorcycle
como estão:1 await carsColl.DeleteManyAsync(FilterDefinition<Car>.Empty);
Se você estiver usando o provedor LINQ oferecido pelo driver C# do MongoDB, também poderá usar o método de extensão LINQ
OfType<T>
para recuperar somente os objetosCar
:1 var cars = vehiclesColl.AsQueryable().OfType<Car>();
Como esperado antes, agora examinamos mais de perto um caso de uso de polimorfismo: suponha que estamos construindo um sistema que ofereça suporte ao monitoramento de sensores distribuídos em vários locais. O sistema deve fornecer uma visão geral que liste todos os sites com seu nome e o último valor que foi relatado para o site, juntamente com um carimbo de data/hora. Ao selecionar um site, o sistema mostra informações detalhadas do site que consistem em todos os dados da visão geral e também lista os sensores localizados no local específico com seu último valor e sua data e hora.
Isso pode ser representado criando uma classe base para os documentos que contêm o ID do site, um nome para identificar o documento e a última medida, se disponível. Uma classe derivada para a visão geral do site adiciona o endereço do site; outro para os detalhes do sensor contém a localização do sensor:
1 using MongoDB.Bson; 2 using MongoDB.Bson.Serialization.Attributes; 3 4 public abstract class BaseDocument 5 { 6 [ ]7 public string Id { get; set; } = ObjectId.GenerateNewId().ToString(); 8 9 [ ]10 public string SiteId { get; set; } = ObjectId.GenerateNewId().ToString(); 11 12 public string Name { get; set; } = string.Empty; 13 14 public Measurement? Last { get; set; } 15 } 16 17 public class Measurement 18 { 19 public int Value { get; set; } 20 21 public DateTime Timestamp { get; set; } 22 } 23 24 public class Address 25 { 26 // ... 27 } 28 29 public class SiteOverview : BaseDocument 30 { 31 public Address Address { get; set; } = new(); 32 } 33 34 public class SensorDetail : BaseDocument 35 { 36 public string Location { get; set; } = string.Empty; 37 }
Ao ingestão de novas medições, tanto a visão geral do site quanto os detalhes do sensor são atualizados (para simplificar, não usamos uma transação de vários documentos):
1 async Task IngestMeasurementAsync( 2 IMongoCollection<BaseDocument> overviewsColl, 3 string sensorId, 4 int value) 5 { 6 var measurement = new Measurement() 7 { 8 Value = value, 9 Timestamp = DateTime.UtcNow 10 }; 11 var sensorUpdate = Builders<SensorDetail> 12 .Update 13 .Set(x => x.Last, measurement); 14 var sensorDetail = await overviewsColl 15 .OfType<SensorDetail>() 16 .FindOneAndUpdateAsync( 17 x => x.Id == sensorId, 18 sensorUpdate, 19 new() { ReturnDocument = ReturnDocument.After }); 20 if (sensorDetail != null) 21 { 22 var siteUpdate = Builders<SiteOverview> 23 .Update 24 .Set(x => x.Last, measurement); 25 var siteId = sensorDetail.SiteId; 26 await overviewsColl 27 .OfType<SiteOverview>() 28 .UpdateOneAsync(x => x.SiteId == siteId, siteUpdate); 29 } 30 }
A amostra acima usa
FindAndUpdateAsync
para atualizar o documento de detalhes do sensor e também recuperar o documento resultante para que o ID do site possa ser determinado. Se o ID do site for conhecido de anteposição, uma atualização simples também poderá ser usada.Ao recuperar os documentos para a visão geral do site, o código a seguir retorna todos os documentos relevantes:
1 var siteOverviews = (await overviewsColl 2 .OfType<SiteOverview>() 3 .FindAsync(FilterDefinition<SiteOverview>.Empty)) 4 .ToEnumerable();
Ao exibir dados detalhados de um site específico, a query a seguir recupera todos os documentos do site por seu ID em uma única solicitação:
1 var siteDetails = await (await overviewsColl 2 .FindAsync(x => x.SiteId == siteId)) 3 .ToListAsync();
O resultado da query pode conter objetos de diferentes tipos; você pode usar o método de extensão LINQ
OfType<T>
na lista para discernir entre os tipos, por exemplo, ao criar um modelo de visualização.Essa abordagem permite uma consulta eficiente de diferentes perspectivas para que as visualizações centrais do aplicativo possam ser atendidas com carga mínima no servidor.
O polimorfismo é uma funcionalidade importante das linguagens orientadas a objetos e há uma ampla variedade de casos de uso para ele. Como você pode ver, o driver C# do MongoDB fornece uma ponte sólida entre a orientação a objetos e o esquema de documentos flexíveis do MongoDB. Se você quiser se afundar no assunto do ponto de vista da modelagem de dados, não deixe de conferir a parte depadrões polimórficos da soberba série "Construindo com padrões" no MongoDB Developer Center.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.