V minulém článku jsme si představili platformu NopCommerce z globálního pohledu. V dnešním díle se již zaměříme na konkrétní část systému, a to datovou vrstvu. Představíme si základní stavební kameny systému v podobě doménových objektů. Ukážeme si, jakým způsobem rozšířit doménové objekty a jakým způsobem přistupuje NopCommerce k nastavení systému a modulů.
Přístup do databáze
Jak jsme již v předchozím díle nastínili, projekt Nop.Data obsahuje datovou vrstvu. Prvním důležitým aspektem, kterým se liší od klasických MVC projektů je skutečnost, že se connection string neuchovává v souboru web.config, ale v souboru AppData/Settings.txt. Třída DataSettingsManager je zodpovědná za načtení připojovacího řetězce do databáze.
Pokud se budete potřebovat dostat v kódu k připojovacímu řetězci, tak jeho nositelem je třída DataSettings. NopCommerce v základu obsahuje dva datové poskytovatele. Pro klasický MS SQL server a pro SQL CE. My budeme dále využívat pouze poskytovatele pro klasické MS SQL.
Pro přístup do databáze se využívá Entity Framework v kombinace s code first přístupem. Servisní vrstva přistupuje do datové vrstvy pomocí generického rozhraní IRepository<T>, na které se dnes taky podíváme.
Inicializace datové vrstvy
Pro pochopení datové vrstvy je vhodné se podívat, jakým způsobem se inicializuje. Třída DataSettingsManager se postará o načtení dat z konfiguračního souboru settings.txt a poté začneme přidávat pravidla pro získávání závislostí do IoC kontaineru. Důležitá třída je NopObjectContext. Jedná se o datový kontext, který dědí od třídy DbContext. Pokud bychom potřebovali získat z IoC kontaineru datový kontext, tak je dostupný pod rozhraním IDbContext.
Repositáře
Při pohledu na kód je viditelné, že rozhraní IRepository<T> se používá pro CRUD operace nad doménovými objekty, kde generický parametr T může být například objekt Customer,Order,BlogPost nebo jakýkoliv jiný bussiness objekt. Rozhraní podporuje i hromadné operace. Je důležité upozornit na vlastnosti Table a TableNoTracking. Přes tyto vlastnosti můžete spouštět LINQ dotazy.
Ostatní metody mají zapouzdřené chování pro CRUD operace v jednotlivých metodách. V případě, že potřebujete pouze číst z databáze, je vhodnější použít vlastnost TableNoTracking, která není tolik náročná na paměť a je optimalizovaná pro výkon. Třída, která implementuje toto rozhraní, je v projektu Nop.Data a jedná se o třídu EfRepository. Z definice rozhraní je vidět, že všechny generické parametry musí dědit od třídy BaseEntity.
using System.Collections.Generic;
using System.Linq;
namespace Nop.Core.Data
{
/// <summary>
/// Repository
/// </summary>
public partial interface IRepository<T> where T : BaseEntity
{
/// <summary>
/// Get entity by identifier
/// </summary>
/// <param name="id">Identifier</param>
/// <returns>Entity</returns>
T GetById(object id);
/// <summary>
/// Insert entity
/// </summary>
/// <param name="entity">Entity</param>
void Insert(T entity);
/// <summary>
/// Insert entities
/// </summary>
/// <param name="entities">Entities</param>
void Insert(IEnumerable<T> entities);
/// <summary>
/// Update entity
/// </summary>
/// <param name="entity">Entity</param>
void Update(T entity);
/// <summary>
/// Update entities
/// </summary>
/// <param name="entities">Entities</param>
void Update(IEnumerable<T> entities);
/// <summary>
/// Delete entity
/// </summary>
/// <param name="entity">Entity</param>
void Delete(T entity);
/// <summary>
/// Delete entities
/// </summary>
/// <param name="entities">Entities</param>
void Delete(IEnumerable<T> entities);
/// <summary>
/// Gets a table
/// </summary>
IQueryable<T> Table { get; }
/// <summary>
/// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations
/// </summary>
IQueryable<T> TableNoTracking { get; }
}
}
Doménové objekty
Jak jsme již výše naznačili, všechny doménové objekty musí dědit od abstraktní třídy BaseEntity. Ta nám zajišťuje společné vlastnosti a metody napříč doménou. V případě, že budete chtít mít společné vlastnosti pro všechny doménové objekty (např. datum vytvoření nebo editace), tak třída BaseEntity je to správné místo, kde začít.
Všechny doménové objekty nalezneme v projektu Nop.Core ve složce Domain. Je jich poměrně dost, ale opět hlavně díky vhodnému a intuitivnímu pojmenování a řazení je celkem jednoduché zjistit účel konkrétních objektů.
Doménový objekt Customer
Můžeme se podívat na jeden ze základních objektů, a to objekt zákazníka. Jak je již nepsané pravidlo NopCommerce, tak všechny vlastnosti jsou vhodně pojmenované. Jak je možné vidět, zákazník má poměrně dost vlastnosti. Co si však pozorný čtenář může všimnout je, že ačkoliv používá NopCommerce code first přístup, tak doménové objekty neobsahují žádné atributy spojené s mapováním.
Z mé zkušenosti je to dobrá praktika. Vyčlenit mapování do jiných souborů a nechat si doménové objekty čisté. Hlavně v případě, že se jedná o větší projekty, kde se může stát, že budete mít více datových kontextů. A také z důvodu, že dodržíte SOLID principy. Kde je tedy mapování objektů?
using System;
using System.Collections.Generic;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
namespace Nop.Core.Domain.Customers
{
/// <summary>
/// Represents a customer
/// </summary>
public partial class Customer : BaseEntity
{
private ICollection<ExternalAuthenticationRecord> _externalAuthenticationRecords;
private ICollection<CustomerRole> _customerRoles;
private ICollection<ShoppingCartItem> _shoppingCartItems;
private ICollection<ReturnRequest> _returnRequests;
private ICollection<Address> _addresses;
/// <summary>
/// Ctor
/// </summary>
public Customer()
{
this.CustomerGuid = Guid.NewGuid();
}
/// <summary>
/// Gets or sets the customer Guid
/// </summary>
public Guid CustomerGuid { get; set; }
/// <summary>
/// Gets or sets the username
/// </summary>
public string Username { get; set; }
/// <summary>
/// Gets or sets the email
/// </summary>
public string Email { get; set; }
/// <summary>
/// Gets or sets the email that should be re-validated. Used in scenarios when a customer is already registered and wants to change an email address.
/// </summary>
public string EmailToRevalidate { get; set; }
/// <summary>
/// Gets or sets the admin comment
/// </summary>
public string AdminComment { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the customer is tax exempt
/// </summary>
public bool IsTaxExempt { get; set; }
/// <summary>
/// Gets or sets the affiliate identifier
/// </summary>
public int AffiliateId { get; set; }
/// <summary>
/// Gets or sets the vendor identifier with which this customer is associated (maganer)
/// </summary>
public int VendorId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this customer has some products in the shopping cart
/// <remarks>The same as if we run this.ShoppingCartItems.Count > 0
/// We use this property for performance optimization:
/// if this property is set to false, then we do not need to load "ShoppingCartItems" navigation property for each page load
/// It's used only in a couple of places in the presenation layer
/// </remarks>
/// </summary>
public bool HasShoppingCartItems { get; set; }
.......
}
}
Mapování doménových objektů pomocí EF
Veškeré mapování objektů je na rozdíl od doménových objektů uložené v projektu Nop.Data, ve složce Mapping. Zde se nachází stejná struktura, jakou mají doménové objekty. Takže se neztratíte až budete hledat mapování pro konkrétní doménový objekt. Samotné nastavení mapování probíhá pomocí standartního fluent api.
using Nop.Core.Domain.Customers;
namespace Nop.Data.Mapping.Customers
{
public partial class CustomerMap : NopEntityTypeConfiguration<Customer>
{
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.Property(u => u.Username).HasMaxLength(1000);
this.Property(u => u.Email).HasMaxLength(1000);
this.Property(u => u.EmailToRevalidate).HasMaxLength(1000);
this.Property(u => u.SystemName).HasMaxLength(400);
this.HasMany(c => c.CustomerRoles)
.WithMany()
.Map(m => m.ToTable("Customer_CustomerRole_Mapping"));
this.HasMany(c => c.Addresses)
.WithMany()
.Map(m => m.ToTable("CustomerAddresses"));
this.HasOptional(c => c.BillingAddress);
this.HasOptional(c => c.ShippingAddress);
}
}
}
Jak tedy probíhá mapování? Pro nalezení všech dostupných mapování pro doménové objekty v systému se používá reflexe, která nalezne všechny třídy, které dědí od objektu NopEntityTypeConfiguration. V našem případě se jedná o třídu CustomerMap a všechny dostupné mapování a předá třídě DbModelBuilder, která se již postará o zpracování. Proces zaregistrování mapování je vidět níže. Mapování by šlo realizovat manuálně, ale při větším množství doménových objektů se stává kód méně přehledný.
Nastavení systému a konfigurační data
Jako každý jiný systém, tak i NopCommerce má poměrně velké množství nejrůznějších nastavení. Jako programátoři zároveň chceme, aby pro každý modul bylo dostupné nastavení, které bude moci administrátor systému měnit. Další věc, na kterou nesmíme opomenout je, že systém NopCommerce umožňuje provozovat více obchodů v rámci jednoho systému. To znamená, že všechna konfigurační nastavení musí obsahovat informaci, pro který obchod jsou platná. Konfigurační data pro systém i pro jednotlivé moduly jsou uloženy v databázi v tabulce Settings.
Rozhraní ISettings
Pokud chcete využívat infrastrukturu připravenou pro práci s konfigurací systému, je nutné implementovat rozhraní ISettings. Implementace je jednoduchá, protože rozhraní nic neobsahuje. Používá se pouze, aby systém dokázal poznat, že se jedná o třídu, které bude uložena do tabulky Setting a aby s třídou dokázala pracovat služba SettingsService.
Můžeme se podívat například na nastavení zákazníka. U Zákazníka můžeme evidovat velmi mnoho údajů od bezpečnostních pravidel až po možnost vytvoření avatara.
Princip konfiguračních dat spočívá v tom, že třída SettingService je schopná jakoukoliv třídu implementující ISettings zapsat do databáze. Všechny vlastnosti třídy implementující ISettings jsou pomocí reflexe převedeny do objektu Settings a uloženy do databáze. V kódu, když potřebujeme načíst nastavení, tak využijeme metodu LoadSettings, která stejným principem načte data z databáze a pomocí reflexe naplní třídu s nastavením. V našem případě CustommerSettings. Třída SettingService také řeší další operace nad daty, jako jsou mazání konfigurační dat nebo jejich aktualizace.