# Database Guide — SOLUTION_ERP > **DB engine:** SQL Server 2022 (LocalDB dev, full instance prod) > **ORM:** EF Core 10 Code-First migrations > **DbContext:** [`ApplicationDbContext`](../../src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs) extends `IdentityDbContext` ## 1. Connection strings | Env | ConnectionString | DB name | |---|---|---| | Dev (default) | `Server=(localdb)\MSSQLLocalDB;Database=SolutionErp_Dev;Trusted_Connection=True` | `SolutionErp_Dev` | | Design-time (EF CLI) | same server, `Database=SolutionErp_Design` | `SolutionErp_Design` | | Prod | `Server=;Database=SolutionErp;User=<>;Password=<>;TrustServerCertificate=true` | `SolutionErp` | Cả 3 DB dùng chung schema, khác tên → có thể drop `SolutionErp_Design` không mất dữ liệu. ## 2. Conventions ### Naming | Item | Rule | Ví dụ | |---|---|---| | Schema | 1 schema duy nhất: `dbo` | — | | Table | PascalCase tiếng Anh, số nhiều | `Contracts`, `Suppliers`, `ContractApprovals` | | Column | PascalCase | `FullName`, `CreatedAt`, `SupplierId` | | PK | `Id` | `Guid` (không auto-increment) | | FK | `{EntityName}Id` | `SupplierId`, `ContractId` | | Index | `IX_{Table}_{Col1}_{Col2}` | `IX_Contracts_Phase_SupplierId` | | Unique | `UX_{Table}_{Col}` | `UX_Suppliers_Code` | | Migration | `YYYYMMDDHHMMSS_{Name}` (auto) | `20260421034520_Init` | ### Data types | C# type | SQL type | Notes | |---|---|---| | `Guid` | `uniqueidentifier` | PK + FK default | | `string` | `nvarchar(max)` default | Luôn set `HasMaxLength(n)` trong config | | `DateTime` | `datetime2` | UTC — luôn qua `IDateTime.UtcNow` | | `decimal` (tiền) | `decimal(18,2)` | `HasPrecision(18, 2)` | | `int` enum | `int` | `HasConversion()` | | `bool` | `bit` | — | | JSON blob | `nvarchar(max)` | `HasColumnType("nvarchar(max)")` — dùng `HasConversion` serialize | ### Audit fields (mọi entity extend `BaseEntity`) ```csharp public Guid Id { get; set; } = Guid.NewGuid(); public DateTime CreatedAt { get; set; } // Set by AuditingInterceptor public DateTime? UpdatedAt { get; set; } // Set by AuditingInterceptor public Guid? CreatedBy { get; set; } // From ICurrentUser.UserId public Guid? UpdatedBy { get; set; } // From ICurrentUser.UserId ``` ### Soft delete (entity extend `AuditableEntity`) ```csharp public bool IsDeleted { get; set; } public DateTime? DeletedAt { get; set; } public Guid? DeletedBy { get; set; } ``` [`AuditingInterceptor`](../../src/Backend/SolutionErp.Infrastructure/Persistence/Interceptors/AuditingInterceptor.cs) tự động: - `Added` entity → set `CreatedAt`, `CreatedBy` - `Modified` entity → set `UpdatedAt`, `UpdatedBy` - `Deleted` entity kiểu `AuditableEntity` → convert sang `Modified` + set `IsDeleted=true`, `DeletedAt`, `DeletedBy` (soft delete) **Query filter soft delete:** config `modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted)` cho mọi `AuditableEntity`. Sẽ làm Phase 1 đợt 2. ## 3. Schema hiện tại (sau migration `Init`) ### Identity tables (đã có sau migration 1) ```mermaid erDiagram Users ||--o{ UserRoles : has Roles ||--o{ UserRoles : has Users ||--o{ UserClaims : has Users ||--o{ UserLogins : has Users ||--o{ UserTokens : has Roles ||--o{ RoleClaims : has Users { uniqueidentifier Id PK nvarchar FullName "200" nvarchar Email nvarchar UserName nvarchar PasswordHash nvarchar RefreshToken "512" datetime2 RefreshTokenExpiresAt bit IsActive datetime2 CreatedAt datetime2 UpdatedAt bit EmailConfirmed bit LockoutEnabled datetime2 LockoutEnd int AccessFailedCount } Roles { uniqueidentifier Id PK nvarchar Name nvarchar NormalizedName nvarchar Description "500" datetime2 CreatedAt } UserRoles { uniqueidentifier UserId PK,FK uniqueidentifier RoleId PK,FK } UserClaims { int Id PK uniqueidentifier UserId FK nvarchar ClaimType nvarchar ClaimValue } UserLogins { nvarchar LoginProvider PK nvarchar ProviderKey PK uniqueidentifier UserId FK } UserTokens { uniqueidentifier UserId PK,FK nvarchar LoginProvider PK nvarchar Name PK nvarchar Value } RoleClaims { int Id PK uniqueidentifier RoleId FK nvarchar ClaimType nvarchar ClaimValue } ``` ### Seed data hiện tại [`DbInitializer`](../../src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs) auto-run khi API start: **12 role:** | Role | Ý nghĩa (theo workflow-contract.md) | |---|---| | `Admin` | Quản trị hệ thống | | `Drafter` | Người soạn thảo (QS/NV.PB) | | `DeptManager` | Trưởng Phòng ban (TPB) | | `ProjectManager` | Giám đốc Dự án (PM) | | `Procurement` | Phòng Cung ứng (PRO) | | `CostControl` | Phòng Kiểm soát Chi phí (CCM) | | `Finance` | Phòng Tài chính (FIN) | | `Accounting` | Phòng Kế toán (ACT) | | `Equipment` | Phòng Thiết bị (EQU) | | `Director` | Ban Giám đốc (BOD) | | `AuthorizedSigner` | Người được ủy quyền ký (NĐUQ) | | `HrAdmin` | Nhân sự - Hành chính (HRA) | **1 admin user:** | Field | Value | |---|---| | Email | `admin@solutionerp.local` | | Password | `Admin@123456` | | FullName | `Administrator` | | Roles | `[Admin]` | ## 4. Schema dự kiến ### Phase 1 đợt 2 — Master data ```mermaid erDiagram Suppliers { uniqueidentifier Id PK nvarchar Code "50 UNIQUE" nvarchar Name "200" nvarchar TaxCode "20" nvarchar Phone "20" nvarchar Email "100" nvarchar Address "500" int Type "NCC/NTP/TD/DVDV" datetime2 CreatedAt datetime2 UpdatedAt uniqueidentifier CreatedBy FK uniqueidentifier UpdatedBy FK bit IsDeleted } Projects { uniqueidentifier Id PK nvarchar Code "50 UNIQUE" nvarchar Name "200" date StartDate date EndDate uniqueidentifier ManagerUserId FK } Departments { uniqueidentifier Id PK nvarchar Code "50 UNIQUE" nvarchar Name "200" uniqueidentifier ManagerUserId FK } Users ||--o{ Projects : manages Users ||--o{ Departments : manages ``` ### Phase 1 đợt 2 — Permission Matrix ```mermaid erDiagram MenuItems ||--o{ Permissions : has Roles ||--o{ Permissions : has MenuItems { nvarchar Key PK "PascalCase, vd Contracts" nvarchar Label "200 tieng Viet" nvarchar ParentKey FK "NULL neu root" int Order nvarchar Icon "50 lucide icon name" } Permissions { uniqueidentifier Id PK uniqueidentifier RoleId FK nvarchar MenuKey FK bit CanRead bit CanCreate bit CanUpdate bit CanDelete } ``` Unique key: `(RoleId, MenuKey)`. ### Phase 3 — Contract + Workflow ```mermaid erDiagram Contracts ||--o{ ContractApprovals : has Contracts ||--o{ ContractComments : has Contracts ||--o{ ContractAttachments : has Contracts }o--|| Suppliers : references Contracts }o--|| Projects : references Contracts }o--|| Departments : drafted_in Contracts }o--|| ContractTemplates : uses Contracts { uniqueidentifier Id PK nvarchar MaHopDong "100 nullable until Phase5" int Type "ContractType enum" int Phase "ContractPhase enum 9 state" uniqueidentifier SupplierId FK uniqueidentifier ProjectId FK uniqueidentifier DepartmentId FK uniqueidentifier DrafterUserId FK uniqueidentifier TemplateId FK decimal GiaTri "18 2" bit BypassProcurementAndCCM datetime2 SlaDeadline "khi nao phase hien tai het han" nvarchar DraftData "nvarchar max JSON field values" } ContractApprovals { uniqueidentifier Id PK uniqueidentifier ContractId FK int Phase uniqueidentifier ApproverUserId FK datetime2 ApprovedAt int Decision "Pending/Approve/Reject/AutoApprove" nvarchar Comment "1000" } ContractComments { uniqueidentifier Id PK uniqueidentifier ContractId FK uniqueidentifier UserId FK int Phase "phase luc comment" nvarchar Content "2000" datetime2 CreatedAt } ContractAttachments { uniqueidentifier Id PK uniqueidentifier ContractId FK nvarchar FileName "255" nvarchar StoragePath "500" bigint FileSize nvarchar ContentType "100" nvarchar Purpose "50 Scan/Export/Template" datetime2 UploadedAt } ContractTemplates { uniqueidentifier Id PK nvarchar FormCode "20 UNIQUE vd FO-002.05" nvarchar Name "200" int ContractType nvarchar TemplatePath "500" nvarchar FieldSpec "nvarchar max JSON" bit IsActive } ContractClauses { uniqueidentifier Id PK nvarchar Code "20 UNIQUE vd FO-002.04" nvarchar Name "200" nvarchar Content "nvarchar max rich text" int Version bit IsActive } ``` ### Phase 3 — Contract Code Generator `ContractCodeSequence` bảng đơn giản để tránh race condition khi gen seq: ```sql CREATE TABLE dbo.ContractCodeSequences ( Prefix NVARCHAR(100) NOT NULL PRIMARY KEY, -- "FLOCK 01/HĐTP/SOL&HD" hoặc "2026/NCC/SOL&AKATI" LastSeq INT NOT NULL, UpdatedAt DATETIME2 NOT NULL ); ``` Logic trong `ContractCodeGenerator`: 1. Begin transaction `SERIALIZABLE` 2. `SELECT LastSeq FROM ContractCodeSequences WITH (UPDLOCK, HOLDLOCK) WHERE Prefix = @prefix` 3. Nếu không có → INSERT với `LastSeq = 1` 4. Nếu có → `UPDATE ContractCodeSequences SET LastSeq = LastSeq + 1` 5. Return `{prefix}/{LastSeq:D2}` 6. Commit ## 5. Migration workflow ### Tạo migration mới ```powershell cd D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP dotnet ef migrations add ` --project src\Backend\SolutionErp.Infrastructure ` --startup-project src\Backend\SolutionErp.Api ` --output-dir Persistence\Migrations ``` Ví dụ Phase 1 đợt 2: ``` dotnet ef migrations add AddMasterData dotnet ef migrations add AddPermissions dotnet ef migrations add AddContractDraft ``` Phase 3: ``` dotnet ef migrations add AddContractWorkflow dotnet ef migrations add AddContractCodeSequence ``` ### Apply migration ```powershell dotnet ef database update ` --project src\Backend\SolutionErp.Infrastructure ` --startup-project src\Backend\SolutionErp.Api ``` Hoặc tự động khi API start (đã config trong `DbInitializer.InitializeAsync` → `db.Database.MigrateAsync()`). ### Revert migration ```powershell # Rollback 1 bước dotnet ef database update # Xóa file migration chưa apply dotnet ef migrations remove ``` ### Revert DB về trạng thái clean ```sql -- Chạy trong SSMS hoặc sqlcmd DROP DATABASE SolutionErp_Dev; -- Hoặc giữ DB, chỉ xóa data: EXEC sp_MSforeachtable 'DELETE FROM ?'; ``` Rồi chạy lại API → tự migrate + seed. ## 6. Index strategy (Phase 3-4 optimize) ### Hot queries cần index | Query | Index đề xuất | |---|---| | Inbox — HĐ theo phase + role của user | `IX_Contracts_Phase_IsDeleted` | | List HĐ theo NCC | `IX_Contracts_SupplierId_IsDeleted` | | List HĐ theo dự án | `IX_Contracts_ProjectId_IsDeleted` | | Approval của 1 HĐ | `IX_ContractApprovals_ContractId_Phase` | | Comment thread | `IX_ContractComments_ContractId_CreatedAt` | | Audit log | `IX_AuditLogs_EntityId_CreatedAt` | ### Index unique | Unique | Cột | |---|---| | `UX_Suppliers_Code` | `Code` | | `UX_Projects_Code` | `Code` | | `UX_Departments_Code` | `Code` | | `UX_Contracts_MaHopDong` | `MaHopDong` (WHERE `MaHopDong IS NOT NULL` — filtered) | | `UX_Users_Email` | đã có qua Identity | | `UX_MenuItems_Key` | PK | | `UX_Permissions_Role_Menu` | `(RoleId, MenuKey)` | ## 7. Backup & restore (prod) ### Backup daily (SQL Agent job) ```sql BACKUP DATABASE SolutionErp TO DISK = 'D:\Backups\SolutionErp_full.bak' WITH INIT, COMPRESSION, CHECKSUM; ``` ### Backup log mỗi 15 phút (nếu full recovery model) ```sql BACKUP LOG SolutionErp TO DISK = 'D:\Backups\SolutionErp_log.trn' WITH INIT; ``` ### Restore ```sql RESTORE DATABASE SolutionErp FROM DISK = 'D:\Backups\SolutionErp_full.bak' WITH REPLACE, RECOVERY; ``` Chi tiết runbook ở Phase 5 `docs/guides/cicd.md`. ## 8. Common pitfalls - **Quên `HasMaxLength`** → `nvarchar(max)`, gây bloat + index lỗi. Luôn set. - **Lưu DateTime local thay vì UTC** → sai timezone khi deploy server khác múi giờ. Dùng `IDateTime.UtcNow` (tiêm qua DI), KHÔNG `DateTime.Now`. - **Soft delete quên filter** → user thấy data đã xóa. Luôn `HasQueryFilter(e => !e.IsDeleted)` ở config. - **FK không có index** → slow join. EF Core tự tạo index cho FK, nhưng vẫn phải verify sau migration. - **Migration bị commit thiếu `.Designer.cs`** → team khác không apply được. Commit cả 3 file (`{name}.cs`, `{name}.Designer.cs`, `ApplicationDbContextModelSnapshot.cs`). - **Chạy `dotnet ef` khi API đang chạy** → lock conflict. Dừng API trước khi migrate ở dev. - **Gen mã HĐ ngoài transaction** → race condition. Luôn dùng `ContractCodeGenerator` với lock. ## 9. Quick SQL cheatsheet ```sql -- Xem 10 HĐ mới nhất SELECT TOP 10 Id, MaHopDong, Phase, CreatedAt FROM Contracts ORDER BY CreatedAt DESC; -- Đếm HĐ theo phase SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase; -- List user + roles SELECT u.Email, u.FullName, r.Name AS Role FROM Users u LEFT JOIN UserRoles ur ON u.Id = ur.UserId LEFT JOIN Roles r ON ur.RoleId = r.Id ORDER BY u.Email; -- Reset admin password (emergency) -- Cách an toàn: không edit PasswordHash trực tiếp, dùng UserManager.ResetPasswordAsync qua API -- Clear toàn bộ dev data (giữ migrations) EXEC sp_MSforeachtable 'DELETE FROM ?'; ```