Praktické použitie MongoDB v .NET
NoSQL databázy naberajú v súčasnosti na populárnosti, dlhšie som sa chystal nejakú z nich vyskúšať. Pri práci na aktuálnom projekte som zvolil riešenie s databázou MongoDB a vyskúšal si praktické použitie tejto databázy na platforme Microsoft .NET.
Pracujem na programe, ktorého úlohou je natiahnuť záznamy z LDAPu a premietnuť ich stav do MS SQL databázy, prioritou je rýchlosť. Keďže LDAP aj MS SQL bežia na iných strojoch ako samotný program, môj nápad bol najprv jednorázovo načítať údaje z LDAPu a serializovať ich do XML súborov, následne pri spracovaní jednotlivých typov objektov si vždy natiahnuť aktuálne záznamy z databázy do pamäti (operácie v pamäti sú výrazne rýchlejšie ako prístup do databázy).
Všetko fungovalo až kým počty záznamov, ktoré bolo potrebné držať súčasne v pamäti nenarazili na hranicu 2 GB pre 32bit .NET Framework (pre lepšiu predstavu uvediem, že v systéme, z ktorého sú dáta exportované existujú rádovo státisíce účtov a grúp, tisíce zamestnancov atď.). Dopytovanie do vzdialenej databázy namiesto držania objektov v pamäti neprichádzalo kvôli rýchlosti do úvahy. Na držanie záznamov v pamäti som využíval hash mapy (Dictionary v .NET), napadlo použiť nejakú NoSQL databázu.
Celý nápad spočíval v tom, že na stroji kde beží export bude bežať lokálne aj NoSQL databáza. Export natiahne záznamy zo vzdialeného MS SQL servera a uloží ich do lokálnej NoSQL databázy namiesto ich ponechania v pamäti. Dotazovanie bude namiesto do hash mapy v pamäti prebiehať do NoSQL databázy, kde kľúč pôvodnej hash mapy bude zodpovedať kľúču NoSQL databázy. Dané riešenie bude určite rýchlejšie ako dopytovanie do vzdialenej MS SQL databázy a nenarazí na limit pamäte.
Ako NoSQL databázu som sa rozhodol využiť MongoDB. Výhodou je, že MongoDB nie je potrebné inštalovať, môže byť teda distribuovaná spolu s vašim programom. Stačí, ak túto NoSQL databázu spustíte pred behom vášho programu a ukončíte spolu s ním. Prípadne to môžete spustenie a ukončenie MongoDB ovládať priamo z vášho programu.
Keďže MongoDB je dokumentová databáza, bolo potrebné ukladané objekty previesť na dokumenty. Ovládač MongoDB pre .NET síce obsahuje možnosť ukladať priamo objekty ľubovoľného typu, nefunguje to však úplne spoľahlivo (občasný StackOverflow v mscore.lib).
Prevod ľubovoľnej triedy na dokument a jej nasledujúcu rekonštrukciu z dokumentu je možné jednoducho vykonať pomocou reflexie, nie je však možné ukladať ako položky dokumentu iné objekty. V mojom prípade boli objekty z MS SQL databázy získavané pomocou LINQ, vytvoril som pre nich preto parciálne triedy:
partial class AccountInTime
{
public Document ToDocument()
{
Document document = new Document();
document["_id"] = AccountId;
Type accountInTimeType = this.GetType();
PropertyInfo[] fieldInfo = accountInTimeType.GetProperties();
string[] bannedProperties = {"User", "Account"};
foreach (PropertyInfo info in fieldInfo)
{
if (!bannedProperties.Contains(info.Name))
{
document[info.Name] = info.GetValue(this, null);
}
}
return document;
}
public AccountInTime(Document document)
{
Type accountInTimeType = this.GetType();
PropertyInfo[] fieldInfo = accountInTimeType.GetProperties();
foreach (PropertyInfo info in fieldInfo)
{
info.SetValue(this,document[info.Name],null);
}
}
}Metóda ToDocument nastaví kľúč _id na vlasnosť AccountId, pomocou reflexie prejde všetky vlastnosti objektu (okrem explicitne zakázaných) a uloží ich do dokumentu. Konštruktor rovnako pomocou reflexie prejde všetky vlastnosti objektu a naplní ich hodnotami z dokumentu.
Následne bola potrebná trieda na prácu s MongoDB, zvolil som Singleton:
internal sealed class MongoRepository
{
private static MongoRepository _mongoRepository;
public static MongoRepository GetInstance()
{
return _mongoRepository ?? (_mongoRepository = new MongoRepository());
}
private readonly Mongo _mongo = new Mongo();
public MongoRepository()
{
_mongo = new Mongo();
_mongo.Connect();
}
}Daný Singleton nie je thread-safe ale v mojom prípadne to nebolo potrebné. V konštruktore sa vytvorí spojenie s MongoDB.
Pri vkladaní záznamov do MongDB najprv všetky existujúce záznamy vymažem (aby reflektovali len stav LDAPu daného dňa) a následne ich vložím prevedené na dokument:
public void Insert(IEnumerable<accountintime> accounts)
{
//databáza
var db = _mongo.GetDatabase("to2");
//kolekcia
var collection = db.GetCollection("accountintimes");
//zmazanie všetkých záznamov
collection.Remove(new Document() { });
foreach (AccountInTime account in accounts)
{
//vloženie záznamu
collection.Insert(account.ToDocument());
}
}Premenná db reprezentuje databázu a premenná collection “tabuľku”. Metóda collection.Remove() maže záznamy vyhovujúce poskytnutému selektoru, new Document() je univerzálny selektor, ktorému vyhovujú všetky záznamy.
Následný prístup k týmto záznamom je možný pomocou IEnumerable rozhrania vhodného na jednorazovú iteráciu:
public IEnumerable<document> AccountInTimes
{
get
{
//databáza
var db = _mongo.GetDatabase("to2");
//kolecia
var collection = db.GetCollection("accountintimes");
//výber všetkých záznamov
return collection.FindAll().Documents;
}
}Ak budeme chcieť následne v MongoDB nejaký záznam vyhľadať pomocou kľúča _id a previesť na objekt pôvodného typu, bude to už veľmi jednoduché:
AccountInTime dbAccountInTime = new AccountInTime(mongoRepository.AccountInTimes.Where(l => l["_id"].ToString() == accountId.ToString()).SingleOrDefault());