in this article, I will show how to take a medium-small demo app written using Visual Studio 2013, ASP.NET 4.5, VC 5, and Entity Framework 6 and turn it into a working ASP.NET 5 app employing Visual Studio 2015, MVC 6 and Entity Framework 7. And the new app will happily run on either the .NET 4.6 CLR or the .NET Core CLR. Let's get started.
ایجاد برنامهی backend ارائه دهندهی REST API
در اینجا یک برنامهی سادهی ASP.NET Core Web API را جهت تدارک سرویسهای backend، مورد استفاده قرار میدهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی میتوانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائهی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. میتوان به این endpintها درخواستهای HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته میشود که جهت آزمایش برنامهها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، میتوان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائهی آرایهای از اشیاء جاوا اسکریپتی قابل استفادهی در برنامههای frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال میکنیم، آرایهای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کردهاست.
اگر علاقمندید بودید میتوانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائهی مطالب آنرا با ASP.NET Core Web API نیز پیاده سازی میکنیم (برای مطالعهی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز میتوان استفاده کرد):
مدل مطالب
namespace sample_22_backend.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } public int UserId { set; get; } } }
منبع دادهی فرضی مطالب
برای ارائهی سادهتر برنامه، یک منبع دادهی درون حافظهای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار میدهیم:
using System; using System.Collections.Generic; using System.Linq; using sample_22_backend.Models; namespace sample_22_backend.Services { public interface IPostsDataSource { List<Post> GetAllPosts(); bool DeletePost(int id); Post AddPost(Post post); bool UpdatePost(int id, Post post); Post GetPost(int id); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> public class PostsDataSource : IPostsDataSource { private readonly List<Post> _allPosts; public PostsDataSource() { _allPosts = createDataSource(); } public List<Post> GetAllPosts() { return _allPosts; } public Post GetPost(int id) { return _allPosts.Find(x => x.Id == id); } public bool DeletePost(int id) { var item = _allPosts.Find(x => x.Id == id); if (item == null) { return false; } _allPosts.Remove(item); return true; } public Post AddPost(Post post) { var id = 1; var lastItem = _allPosts.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } post.Id = id; _allPosts.Add(post); return post; } public bool UpdatePost(int id, Post post) { var item = _allPosts .Select((pst, index) => new { Item = pst, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null || id != post.Id) { return false; } _allPosts[item.Index] = post; return true; } private static List<Post> createDataSource() { var list = new List<Post>(); var rnd = new Random(); for (var i = 1; i < 10; i++) { list.Add(new Post { Id = i, UserId = rnd.Next(1, 1000), Title = $"Title {i} ...", Body = $"Body {i} ..." }); } return list; } } }
کنترلر Web API برنامهی backend
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using sample_22_backend.Models; using sample_22_backend.Services; namespace sample_22_backend.Controllers { [ApiController] [Route("api/[controller]")] public class PostsController : ControllerBase { private readonly IPostsDataSource _postsDataSource; public PostsController(IPostsDataSource postsDataSource) { _postsDataSource = postsDataSource; } [HttpGet] public ActionResult<List<Post>> GetPosts() { return _postsDataSource.GetAllPosts(); } [HttpGet("{id}")] public ActionResult<Post> GetPost(int id) { var post = _postsDataSource.GetPost(id); if (post == null) { return NotFound(); } return Ok(post); } [HttpDelete("{id}")] public ActionResult DeletePost(int id) { var deleted = _postsDataSource.DeletePost(id); if (deleted) { return Ok(); } return NotFound(); } [HttpPost] public ActionResult<Post> CreatePost([FromBody]Post post) { post = _postsDataSource.AddPost(post); return CreatedAtRoute(nameof(GetPost), new { post.Id }, post); } [HttpPut("{id}")] public ActionResult<Post> UpdatePost(int id, [FromBody]Post post) { var updated = _postsDataSource.UpdatePost(id, post); if (updated) { return Ok(post); } return NotFound(); } } }
البته نمایش فرمت شدهی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.
ایجاد ساختار ابتدایی برنامهی ارتباط با سرور
در اینجا برای بررسی کار با سرور، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app sample-22-frontend > cd sample-22-frontend > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
سپس فایل app.js را به شکل زیر تکمیل میکنیم:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { posts: [] }; handleAdd = () => { console.log("Add"); }; handleUpdate = post => { console.log("Update", post); }; handleDelete = post => { console.log("Delete", post); }; render() { return ( <React.Fragment> <button className="btn btn-primary mt-1 mb-1" onClick={this.handleAdd}> Add </button> <table className="table"> <thead> <tr> <th>Title</th> <th>Update</th> <th>Delete</th> </tr> </thead> <tbody> {this.state.posts.map(post => ( <tr key={post.id}> <td>{post.title}</td> <td> <button className="btn btn-info btn-sm" onClick={() => this.handleUpdate(post)} > Update </button> </td> <td> <button className="btn btn-danger btn-sm" onClick={() => this.handleDelete(post)} > Delete </button> </td> </tr> ))} </tbody> </table> </React.Fragment> ); } } export default App;
نگاهی به انواع و اقسام HTTP Clientهای مهیا
در ادامه نیاز خواهیم داشت تا از طریق برنامههای React خود، درخواستهای HTTP را به سمت سرور (یا همان برنامهی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راهحل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت میتوانید از کتابخانههایی که خودتان ترجیح میدهید، نسبت به کتابخانههایی که به شما ارائه/تحمیل (!) میشوند (مانند برنامههای Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شدهاست! ابتدا با یک کتابخانهی HTTP مقدماتی شروع کردند. بعدی آنرا منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابستهاست به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار میکنید، باید کارها را آنگونه که Angular میپسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح میشود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمیتر آنها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید میتوانید از محصور کنندههای آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی میتوانید از آن در برنامههای React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانهی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژهی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!
نصب Axios در برنامهی React این قسمت
برای نصب کتابخانهی Axios، در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
نکاتی در باب T-SQL
Create Table Test(ID int,Firstname varchar(20)); Insert Into Test (ID,Firstname) Values(1,'K'); Insert Into Test (ID,Firstname) Values(2,'B'); Insert Into Test (ID) Values(3);
Select COUNT(*) From Test
Select COUNT(Firstname) From Test
- هرگاه در اجرای Count ،هدفتان بدست آوردن تعداد ستون خاصی است، از (Count(column استفاده نمایید.
select count(cast(null as int)) select count(*) where 'a'='b' select € select ¥ select £ Select $ select count(*)-count(*) select Ascii('A')-Ascii('A') select LEN('')
- روش اول
(a+b)+ABS(a-b)
0.5(a+b)+ABS(a-b)
DECLARE @Value1 DECIMAL(5,2) = 80.22 DECLARE @Value2 DECIMAL(5,2) =90.34 SELECT (0.5 * ((@Value1 + @Value2) + ABS(@Value1 - @Value2))) AS MaxColumn
- روش دوم
DECLARE @Value1 DECIMAL(5,2) = 9.22 DECLARE @Value2 DECIMAL(5,2) = 8.34 SELECT CASE WHEN @Value1 > @Value2 THEN @Value1 ELSE @Value2 END AS MaxColumn
در این روش اگر مقدار یکی از اعداد Null باشد،ماکزیمم بین دو عدد، عدد غیر Null میباشد.
5- مشاهده مشخصات کلیه دیتابیسهای موجود در SQL Server با استفاده از Sys.Databases .
Select * From sys.databases
خروجی بصورت زیر خواهد بود:
6- بوسیله دستور OUTPUT میتوان خروجی Queryهای Delete،Update و Insert را مشاهده نمود:
مثال اول برای Query Delete :
در شکل، تک رکورد حذف شده را مشاهده مینمایید.
مثال دوم برای Query Update بصورت زیر میباشد:
در شکل، مقدار A، مقدار جدیدی است که بروز رسانی شده است و مقدار B مقداری است که مربوط به قبل از بروز رسانی میباشد.
مثال سوم برای Query Insert بصورت زیر میباشد:
موفق باشید.
تعریف موجودیت و DbSet تصاویر یک اتاق هتل
برای اینکه بتوان اطلاعات تصاویر آپلودی را در بانک اطلاعاتی ثبت کرد، نیاز است یک رابطهی یک به چند را بین یک اتاق و تصاویر مرتبط با آن برقرار کرد. به همین جهت ابتدا به پروژهی BlazorServer.Entities.csproj مراجعه کرده و موجودیت ثبت اطلاعات تصاویر را تعریف میکنیم:
using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class HotelRoomImage { public int Id { get; set; } public string RoomImageUrl { get; set; } [ForeignKey("RoomId")] public virtual HotelRoom HotelRoom { get; set; } public int RoomId { get; set; } } }
namespace BlazorServer.Entities { public class HotelRoom { // ... public virtual ICollection<HotelRoomImage> HotelRoomImages { get; set; } } }
namespace BlazorServer.DataAccess { public class ApplicationDbContext : DbContext { public DbSet<HotelRoomImage> HotelRoomImages { get; set; } // ... } }
dotnet tool update --global dotnet-ef --version 5.0.3 dotnet build dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
تعریف مدل UI متناظر با هر تصویر
همانطور که در قسمت 13 نیز عنوان شد، در حین کار با رابط کاربری برنامه، با موجودیتهای بانک اطلاعاتی، به صورت مستقیم کار نخواهیم کرد و بر اساس نیازهای برنامه، یکسری کلاس DTO را تعریف میکنیم. بنابراین به پروژهی BlazorServer.Models مراجعه کرده و DTO متناظر با HotelRoomImage را به صورت زیر اضافه میکنیم:
namespace BlazorServer.Models { public class HotelRoomImageDTO { public int Id { get; set; } public int RoomId { get; set; } public string RoomImageUrl { get; set; } } }
using AutoMapper; using BlazorServer.Entities; namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { // ... CreateMap<HotelRoomImageDTO, HotelRoomImage>().ReverseMap(); // two-way mapping } } }
تعریف سرویس کار با HotelRoomImage
در اینجا نیز همانند سرویسی که برای انجام عملیات تجاری مرتبط با یک اتاق هتل، در قسمت 13 پیاده سازی کردیم، سرویس دیگری را در پروژهی BlazorServer.Services برای کار با تصاویر اتاقها تهیه میکنیم:
namespace BlazorServer.Services { public interface IHotelRoomImageService { Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO); Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId); Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId); Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId); } }
namespace BlazorServer.Services { public class HotelRoomImageService : IHotelRoomImageService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public HotelRoomImageService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO) { var image = _mapper.Map<HotelRoomImage>(imageDTO); await _dbContext.HotelRoomImages.AddAsync(image); return await _dbContext.SaveChangesAsync(); } public async Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId) { var image = await _dbContext.HotelRoomImages.FindAsync(imageId); _dbContext.HotelRoomImages.Remove(image); return await _dbContext.SaveChangesAsync(); } public async Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId) { var imageList = await _dbContext.HotelRoomImages.Where(x => x.RoomId == roomId).ToListAsync(); _dbContext.HotelRoomImages.RemoveRange(imageList); return await _dbContext.SaveChangesAsync(); } public Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId) { return _dbContext.HotelRoomImages .Where(x => x.RoomId == roomId) .ProjectTo<HotelRoomImageDTO>(_mapperConfiguration) .ToListAsync(); } } }
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHotelRoomImageService, HotelRoomImageService>(); // ...
تهیه سرویسی برای آپلود فایلهای یک برنامهی Blazor Server به سرور
جهت ساده سازی کار آپلود، در برنامههای Blazor Server، سرویس جدید FileUploadService را به پروژهی BlazorServer.Services اضافه میکنیم:
using Microsoft.AspNetCore.Components.Forms; using System.Threading.Tasks; namespace BlazorServer.Services { public interface IFileUploadService { void DeleteFile(string fileName, string webRootPath, string uploadFolder); Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder); } }
using Microsoft.AspNetCore.Components.Forms; using System; using System.IO; using System.Threading.Tasks; namespace BlazorServer.Services { public class FileUploadService : IFileUploadService { private const int MaxBufferSize = 0x10000; public void DeleteFile(string fileName, string webRootPath, string uploadFolder) { var path = Path.Combine(webRootPath, uploadFolder, fileName); if (File.Exists(path)) { File.Delete(path); } } public async Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder) { createUploadDir(webRootPath, uploadFolder); var (fileName, imageFilePath) = getOutputFileInfo(inputFile, webRootPath, uploadFolder); using (var outputFileStream = new FileStream( imageFilePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true)) { using var inputStream = inputFile.OpenReadStream(); await inputStream.CopyToAsync(outputFileStream); } return $"{uploadFolder}/{fileName}"; } private static (string FileName, string FilePath) getOutputFileInfo( IBrowserFile inputFile, string webRootPath, string uploadFolder) { var fileName = Path.GetFileName(inputFile.Name); var imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName); if (File.Exists(imageFilePath)) { var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); var fileExtension = Path.GetExtension(fileName); fileName = $"{fileNameWithoutExtension}-{Guid.NewGuid()}{fileExtension}"; imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName); } return (fileName, imageFilePath); } private static void createUploadDir(string webRootPath, string uploadFolder) { var folderDirectory = Path.Combine(webRootPath, uploadFolder); if (!Directory.Exists(folderDirectory)) { Directory.CreateDirectory(folderDirectory); } } } }
همچنین برای دسترسی به IBrowserFile در یک سرویس، نیاز است وابستگی زیر را نیز به پروژهی سرویسها اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" /> </ItemGroup> </Project>
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IFileUploadService, FileUploadService>(); // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-16.zip
services.AddOptions<BearerTokensOptions>() .Bind(configuration.GetSection("BearerTokens")) .Validate(bearerTokens => { return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes; }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
معرفی تنظیمات برنامه
فرض کنید فایل appsettings.json برنامه یک چنین محتوایی را دارد:
{ "ApiSettings": { "AllowedEndpoints": [ { "Name": "Service 1", "Timeout": 30, "Url": "http://service1.site.com" }, { "Name": "Service 2", "Timeout": 10, "Url": "https://service2.site.com" } ] } }
ایجاد مدلهای معادل تنظیمات JSON برنامه
بر اساس تعاریف JSON فوق، میتوان به مدلهای زیر رسید:
using System; using System.Collections.Generic; namespace FluentValidationSample.Models { public class AllowedEndpoint { public string Name { get; set; } public int Timeout { get; set; } public Uri Url { get; set; } } public class ApiSettings { public IEnumerable<AllowedEndpoint> AllowedEndpoints { get; set; } } }
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
تعریف شرطهای اعتبارسنجی مدلهای تنظیمات برنامه
پس از مدلسازی تنظیمات برنامه و همچنین اتصال آن به <IOptions<ApiSettings، اکنون میخواهیم این مدلها، شرایط زیر را برآورده کنند:
- باید مدخل ApiSettings در فایل تنظیمات برنامه وجود خارجی داشته باشد.
- میخواهیم AllowedEndpointها نامدار بوده و هر نام نیز منحصربفرد باشد.
- مقادیر timeoutها باید بین 1 و 90 تعریف شده باشند.
- تمام URLها باید منحصربفرد باشند.
- تمام URLها باید HTTPS باشند.
برای این منظور میتوان تنظیمات زیر را توسط Fluent Validation تعریف کرد:
using System; using System.Linq; using FluentValidation; using FluentValidationSample.Models; namespace FluentValidationSample.ModelsValidations { public class ApiSettingsValidator : AbstractValidator<ApiSettings> { public ApiSettingsValidator() { RuleFor(apiSetting => apiSetting).NotNull() .WithMessage("مدخل ApiSettings تعریف نشدهاست."); RuleFor(apiSetting => apiSetting.AllowedEndpoints).NotNull().NotEmpty() .WithMessage("مدخل AllowedEndpoints تعریف نشدهاست."); When(apiSetting => apiSetting.AllowedEndpoints != null, () => { RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Name).Count() == endpoints.Count()) .WithMessage("نامهای سرویسها باید منحصربفرد باشند."); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => !endpoints.Any(endpoint => endpoint.Timeout > 90 || endpoint.Timeout < 1)) .WithMessage("مقدار timeout باید بین 1 و 90 باشد"); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Url.ToString().ToLower()).Count() == endpoints.Count()) .WithMessage("آدرسهای سرویسها باید منحصربفرد باشند."); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.All(endpoint => endpoint.Url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase))) .WithMessage("تمام آدرسها باید HTTPS باشند."); }); } } }
- چگونه میتوان از تعریف و وجود یک مدخل فایل JSON، اطمینان حاصل کرد (اعمال RuleFor به کل مدل).
- چگونه میتوان اگر مدخلی تعریف شده بود، آنگاه برای آن اعتبارسنجی خاصی را تعریف کرد (متد When).
- چگونه میتوان شرایط سفارشی خاصی را مانند بررسی منحصربفرد بودنها، بررسی کرد (متد Must).
یکپارچه کردن اعتبارسنجی کتابخانهی FluentValidation با اعتبارسنجی توکار مدلهای تنظیمات برنامه توسط ASP.NET Core
در ابتدای بحث، امکان تعریف متد Validate را که از نگارش ASP.NET Core 2.2 اضافه شدهاست، مشاهده کردید:
services.AddOptions<BearerTokensOptions>() .Bind(configuration.GetSection("BearerTokens")) .Validate(bearerTokens => { return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes; }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
namespace Microsoft.Extensions.Options { public interface IValidateOptions<TOptions> where TOptions : class { ValidateOptionsResult Validate(string name, TOptions options); } }
در ادامه یک نمونه پیاده سازی جنریک IValidateOptions استاندارد ASP.NET Core را مشاهده میکنید:
using System.Linq; using FluentValidation; using Microsoft.Extensions.Options; namespace FluentValidationSample.ModelsValidations { public class AppConfigValidator<TOptions> : IValidateOptions<TOptions> where TOptions : class { private readonly IValidator<TOptions> _validator; public AppConfigValidator(IValidator<TOptions> validator) { _validator = validator; } public ValidateOptionsResult Validate(string name, TOptions options) { if (options is null) { return ValidateOptionsResult.Fail("Configuration object is null."); } var validationResult = _validator.Validate(options); return validationResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validationResult.Errors.Select(error => error.ToString())); } } }
در آخر روش معرفی آن به سیستم به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings))); services.AddTransient<IValidateOptions<ApiSettings>, AppConfigValidator<ApiSettings>>();
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IUsersService _usersService; private readonly ApiSettings _apiSettings; public HomeController(IUsersService usersService, IOptions<ApiSettings> apiSettings) { _usersService = usersService; _apiSettings = apiSettings.Value; }
An unhandled exception occurred while processing the request. OptionsValidationException: تمام آدرسها باید HTTPS باشند.
کدهای کامل این سری را تا این قسمت از اینجا میتوانید دریافت کنید: FluentValidationSample-part05.zip
NuGet چیست؟
روش متداول استفاده از کتابخانههای موجود دات نتی در Visual studio عموما به این صورت است: مراجعه به سایت مربوطه، دریافت بسته مورد نظر، باز کردن آن و سپس افزودن ارجاعی به اسمبلیهای آن کتابخانه. در این حالت زمانیکه نسخهی جدیدی از کتابخانهی مورد استفاده ارائه شود (و عموما تا مدتها شاید از آن بیاطلاع باشیم) تمام این مراحل باید از ابتدا تکرار شوند و همینطور الی آخر.
برای رفع این نقیصه، تیم ASP.NET، افزونهای سورس باز و رایگان را به نام NuGet جهت VS.Net 2010 طراحی کردهاند که کار مدیریت بستههای کتابخانههای مورد استفاده را بسیار ساده کرده است. امکانات این افزونه پس از نصب، در دو حالت استفاده از رابط گرافیکی کاربری آن و یا با استفاده از خط فرمان PowerShell ویندوز در دسترس خواهد بود. این افزونه در زمان بارگذاری، با مراجعه به فید سایت مرکزی خود، لیست بستههای مهیا را در اختیار علاقمندان قرار میدهد. درب این سایت مرکزی به روی تمام توسعه دهندهها جهت افزودن بستههای خود باز است.
و ... فراگیری کار با NuGet برای تمام برنامه نویسان دات نت لازم و ضروری است! از این جهت که پیغام "این بسته تنها برای NuGet عرضه شده است" کم کم در حال متداول شدن میباشد و دیگر سایتهای مرتبط، لینک مستقیمی را جهت دریافت کتابخانههای خود ارائه نمیدهند. حتی خبر به روز شدن محصولات خود را هم شاید دیگر به صورت منظم ارائه ندهند؛ زیرا NuGet کار مدیریت آنها را به عهده خواهد داشت.
دریافت و نصب NuGet
NuGet را حداقل به سه طریق میتوان دریافت و نصب کرد:
الف) با مراجعه به سایت CodePlex : (+)
ب) دریافت آن از سایت گالریهای آن : (+)
ج) با استفاده از امکانات VS.NET
هر سه روش فوق به دریافت و نصب فایل NuGet.Tools.vsix منتهی میشوند. برای مثال در روش (ج) باید به منوی Tools و گزینهی Extension Manager مراجعه کنید. سپس برگهی Online Gallery را گشوده و اندکی صبر کنید تا اطلاعات آن دریافت و نمایش داده شود. سپس NuGet را در Search box بالای صفحه نوشته و NuGet Package manager ظاهر شده را انتخاب و نصب کنید.
نحوه استفاده از NuGet
فرض کنید یک پروژه جدید ASP.NET را ایجاد کردهاید و نیاز است تا کتابخانهی ELMAH به آن اضافه شود. روش انجام اینکار را به کمک NuGet در ادامه بررسی خواهیم کرد (کمتر از یک دقیقه زمان خواهد برد):
الف) با کمک امکانات رابط گرافیکی کاربر آن
سادهترین روش استفاده از NuGet ، کلیک راست بر روی پوشه References در Solution explorer و سپس انتخاب گزینهی Add Library Package Reference میباشد:
در صفحهی باز شده، برگهی Online را باز کنید و مدتی صبر نمائید تا اطلاعات لازم دریافت گردد (در زمان نگارش این مطلب، 1135 بسته در این مخزن موجود است):
سپس در جعبهی جستجوی سمت راست بالای صفحه، نام کتابخانهی مورد نظر را نوشته و اندکی صبر کنید تا اطلاعات آن نمایش داده شود:
اکنون با کلیک بر روی دکمه Install ، بسته مرتبط با این کتابخانه دریافت شده و سپس به صورت خودکار ارجاعی به آن نیز افزوده خواهد شد. همچنین تنظیمات مرتبط با فایل Config برنامه هم اضافه میشوند.
روش دیگر ظاهر کردن این صفحه، مراجعه به منوی Tools و گزینهی Library Package Manager میباشد:
جهت دریافت به روز رسانیهای بستههای نصب شده تنها کافی است به برگهی Updates این صفحه مراجعه کرده و موارد لیست شده را نصب نمائیم:
نکته: NuGet در SharpDevelop 4.1 به بعد هم پشتیبانی میشود:
ب) با استفاده از امکانات خط فرمان PowerShell ویندوز
برای استفاده از امکانات پاورشل ویندوز نیاز است تا پاورشل نگارش 2 بر روی سیستم شما نصب باشد (نیاز به Windows XP with Service Pack 3 به بعد دارد). سپس به منوی Tools ، قسمت Library Package Manager ، گزینهی Package Manager Console آن جهت فعال سازی کنسول پاور شل در VS.NET مراجعه نمائید:
نکته: در تصویر فوق پس از نوشتن el ، دکمه tab فشرده شده است. در این حالت منوی پکیجهای مهیای شروع شده با el، از سایت مرکزی NuGet ظاهر گردیده است.
فرامین مهمی که در اینجا در دسترس هستند شامل: List-Package ، Uninstall-Package ، Update-Package و Get-Package میباشند. برای مثال اگر قصد جستجو در بین بستههای موجود را داشته باشید Get-Package بسیار مفید است:
برای مثال جهت یافتن بستههای مرتبط با wpf و silverlight به صورت زیر میتوان عمل کرد:
PM> get-package -remote -filter wpf
PM> get-package -remote -filter silverlight
نکته: روش دیگر جستجو در بین بستههای مهیا، مراجعه به سایت گالری آن است : (+) . در اینجا دستور پاورشل نصب هر بستهی یافت شده نیز نمایش داده میشود.
ج) استفاده از برنامه NuGet.exe
برنامه NuGet.exe از سایت CodePlex قابل دریافت است. این روش هم جهت علاقمندان به خط فرمان تدارک دیده شده است!
پس از دریافت آن فرض کنید میخواهیم تمام بستههای شروع شده با nhi را لیست کنیم. برای این منظور دستور خط فرمان ذیل را صادر کنید:
D:\Test>nuget list nhi
D:\Test>nuget install NHibernate
به این صورت کتابخانه NHibernate به همراه تمام وابستگیهای آن دریافت خواهد شد.
به روز رسانی خودکار NuGet
برای به روز رسانی برنامه nuget.exe دستور زیر را میتوان صادر نمود:
D:\Test>NuGet.exe u
Tools | Options, then Environment | Extension Manager and click "Automatically check for updates to installed extensions."
ادامه دارد ...
آشنایی با Refactoring - قسمت 3
public class OrderLineItem : Entity { /// <summary> /// many-to-one from OrderLineItem to Order /// </summary> public virtual Order Order { get; set; } /// <summary> /// Money is a component, not a separate entity; i.e., the OrderLineItems table will have /// column for the amount /// </summary> public virtual Money Price { get; set; } /// <summary> /// many-to-one from OrderLineItem to Product /// </summary> public virtual Product Product { get; set; } public virtual int Quantity { get; set; } /// <summary> /// Example of adding domain business logic to entity /// </summary> public virtual Money GetTotal() { return new Money(Price.Amount * Quantity); } }
SELECT * FROM table1 WHERE OrderDate ='12 Mar 2004' SET @SQL = 'SELECT * FROM table2 WHERE OrderDate = ' + '''' + @Var + '''' EXEC (@SQL)
این نوع مشکلات با بکار گیری ORMها به نحو قابل توجهی کاهش یافتهاست؛ زیرا این نوع واسطها در اغلب موارد، در آخر کار کوئریهایی پارامتری را تولید میکنند.
مشکل کوئریهای غیر پارامتری چیست؟
استفادهی وسیع از کوئریهای غیرپارامتری با SQL Server، مشکلی را پدید میآورد به نام «Cache bloat» یا «کش پُف کرده» و این «پُف» به این معنا است که کش کوئریهای اجرا شدهی بر روی SQL Server بیش از اندازه با Query planهای مختلف حاصل از بررسی نحوهی اجرای بهینهی آنها پر شدهاست. هر کوئری که به SQL Server میرسد، جهت اجرای بهینه، ابتدا پردازش میشود و دستور العملی خاص آن، تهیه و سپس در حافظه کش میشود. وجود این کش به این خاطر است که SQL Server هربار به ازای هر کوئری رسیده، این عملیات پردازشی را تکرار نکند. مشکل از زمانی شروع میشود که SQL Server کوئریهایی را که از نظر یک برنامه نویس مانند هم هستند را به علت عدم استفادهی از پارامترها، یکسان تشخیص نداده و برای هر کدام یک Plan جداگانه را محاسبه و کش میکند. این مساله با حجم بالای کوئریهای رسیده دو مشکل را ایجاد میکند:
الف) مصرف حافظهی بالای SQL Server که گاهی اوقات این حافظهی اختصاص داده شدهی به کش کوئریها به بالای یک گیگابایت نیز میرسد.
ب) CPU Usage بالای سیستم
سیستم قدیمی است؛ امکان تغییر کدها را نداریم.
بدیهی است بهترین راه حلی که در اینجا وجود دارد، پارامتری ارسال کردن کوئریها به SQL Server است تا به ازای هر تغییری در مقادیر آنها، این کوئریها باز هم یکسان به نظر برسند و SQL Server سعی در محاسبهی مجدد Plan آنها نکند. اما ... اگر این امکان را ندارید، خود SQL Server یک چنین قابلیتهایی را به صورت توکار تدارک دیدهاست که باید فعال شوند.
فعال سازی پارامتری کردن خودکار کوئریها در SQL Server
اگر نمیتوانید کدهای یک سیستم قدیمی را تغییر دهید، SQL Server میتواند به صورت خودکار اینکار را برای شما انجام دهد. در این حالت فقط کافی است یکی از دو دستور ذیل را اجرا کنید:
--Forced ALTER DATABASE dbName SET PARAMETERIZATION FORCED --Simple ALTER DATABASE dbName SET PARAMETERIZATION SIMPLE
فعال سازی بهبود کارآیی SQL Server با کوئریهای Ad-Hoc زیاد
به کوئریهای غیرپارامتری، کوئریهای Ad-Hoc نیز گفته میشود. اگر سیستم فعلی شما، تعداد زیادی کوئری Ad-Hoc تولید میکند، میتوان فشار کاری SQL Server را برای این مورد خاص، تنظیم و بهینه سازی کرد.
فعال سازی گزینهی ویژهی «Optimize for Ad hoc Workloads» سبب میشود تا SQL Server پس از مدتی به صورت خودکار کش Plan کوئریهایی را که به ندرت استفاده میشوند، حذف کند. همین مساله سبب آزاد شدن حافظه و بهبود کارآیی کلی سیستم میگردد. همچنین باید درنظر داشت که کش Plan کوئریها نامحدود نیست و سقفی دارد. به همین جهت آزاد شدن آن، کش کردن کوئریهایی را که بیشتر استفاده میشوند، سادهتر میکند.
برای اعمال آن به یک بانک اطلاعاتی خاص، نیاز است دستورات ذیل را اجرا کرد:
use dbName; -- Optimizing for Ad hoc Workloads exec sp_configure 'show advanced options',1; RECONFIGURE; go exec sp_configure 'optimize for ad hoc workloads',1; RECONFIGURE; Go
برای مطالعهی بیشتر
Fixing Cache Bloat Problems With Guide Plans and Forced Parameterization
Optimizing ad-hoc workloads
Optimizing for Ad hoc Workloads
SQL Antipattern #2
بخش دوم : Naive Trees
فرض کنید یک وب سایت حرفهای خبری یا علمی-پژوهشی داریم که قابلیت دریافت نظرات کاربران را در مورد هر مطلب مندرج در سایت یا نظرات داده شده در مورد آن مطالب را دارا میباشد. یعنی هر کاربر علاوه بر توانایی اظهار نظر در مورد یک خبر یا مطلب باید بتواند پاسخ نظرات کاربران دیگر را نیز بدهد. اولین راه حلی که برای طراحی این مطلب در پایگاه داده به ذهن ما میرسد، ایجاد یک زنجیره با استفاده از کد sql زیر میباشد:
CREATE TABLE Comments ( comment_idSERIAL PRIMARY KEY, parent_idBIGINT UNSIGNED, comment TEXT NOT NULL, FOREIGN KEY (parent_id) REFERENCES Comments(comment_id) );
البته همان طور که پیداست بازیابی زنجیرهای از پاسخها در یک پرسوجوی sql کار سختی است. این نخها معمولا عمق نامحدودی دارند و برای به دست آوردن تمام نخهای یک زنجیره باید پرسوجوهای زیادی را اجرا نمود.
ایدهی دیگر میتواند بازیابی تمام نظرها و ذخیرهی آنها در حافظهی برنامه به صورت درخت باشد. ولی این روش برای ذخیره هزاران نظری که ممکن است در سایت ثبت شود و علاوه بر آن مقالات جدیدی که اضافه میشوند، تقریبا غیرعملی است.
1.2 هدف: ذخیره و ایجاد پرسوجو در سلسلهمراتب
وجود سلسله مراتب بین دادهها امری عادی محسوب میگردد. در ساختار دادهای درختی هر ورودی یک گره محسوب میگردد. یک گره ممکن است تعدادی فرزند و یک پدر داشته باشد. گره اول پدر ندارد، ریشه و گره فرزند که فرزند ندارد، برگ و گرهای دیگر، گرههای غیربرگ نامیده میشوند.
مثالهایی که از ساختار درختی دادهها وجود دارد شامل موارد زیر است:
Organizational chart: در این ساختار برای مثال در ارتباط بین کارمندان و مدیر، هر کارمند یک مدیر دارد که نشاندهندهی پدر یک کارمند در ساختار درختی است. هر مدیر هم یک کارمند محسوب میگردد.
Threaded discussion: در این ساختار برای مثال در سیستم نظردهی و پاسخها، ممکن است زنجیرهای از نظرات در پاسخ به نظرات دیگر استفاده گردد. در درخت، فرزندان یک گرهی نظر، پاسخهای آن گره هستند.
در این فصل ما از مثال ساختار دوم برای نشان دادن Antipattern و راه حل آن بهره میگیریم.
2.2 Antipattern : همیشه مبتنی بر یکی از والدین
راه حل ابتدایی و ناکارآمد
اضافه نمودن ستون parent_id . این ستون، به ستون نظر در همان جدول ارجاع داده میشود و شما میتوانید برای اجرای این رابطه از قید کلید خارجی استفاده نمایید. پرسوجویی که برای ساخت مثالی که ما در این بحث از آن استفاده میکنیم در ادامه آمده است:
CREATE TABLE Comments ( comment_idSERIAL PRIMARY KEY, parent_idBIGINT UNSIGNED, bug_idBIGINT UNSIGNED NOT NULL, author BIGINT UNSIGNED NOT NULL, comment_dateDATETIME NOT NULL, comment TEXT NOT NULL, FOREIGN KEY (parent_id)REFERENCES Comments(comment_id), FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id), FOREIGN KEY(author) REFERENCES Accounts(account_id) );
لیست مجاورت :
لیست مجاورت خود میتواند به عنوان یک antipattern در نظر گرفته شود. البته این مطلب از آنجایی نشأت میگیرد که این روش توسط بسیاری از توسعهدهندگان مورد استفاده قرار میگیرد ولی نتوانسته است به عنوان راه حل برای معمولترین وظیفهی خود، یعنی ایجاد پرسوجو بر روی کلیه فرزندان، باشد.
• با استفاده از پرسوجوی زیر میتوان فرزند بلافاصلهی یک "نظر" را برگرداند:
SELECT c1.*, c2.* FROM Comments c1 LEFT OUTER JOIN Comments c2 ON c2.parent_id = c1.comment_id;
ضعف پرسوجوی فوق این است که فقط میتواند دو سطح از درخت را برای شما برگرداند. در حالیکه خاصیت درخت این است که شما را قادر میسازد بدون هیچ گونه محدودیتی فرزندان و نوههای متعدد (سطوح بیشمار) برای درخت خود تعریف کنید. بنابراین به ازای هر سطح اضافه باید یک join به پرسجوی خود اضافه نمایید. برای مثال اگر پرسوجوی زیر میتواند درختی با چهار سطح برای شما برگرداند ولی نه بیش از آن:
SELECT c1.*, c2.*, c3.*, c4.* FROM Comments c1 -- 1st level LEFT OUTER JOIN Comments c2 ON c2.parent_id = c1.comment_id -- 2nd level LEFT OUTER JOIN Comments c3 ON c3.parent_id = c2.comment_id -- 3rd level LEFT OUTER JOIN Comments c4 ON c4.parent_id = c3.comment_id; -- 4th level
این پرسوجو به این دلیل که با اضافه شدن ستونهای دیگر، نوهها را سطوح عمیقتری برمیگرداند، پرسوجوی مناسبی نیست. در واقع استفاده از توابع تجمیعی ، مانند COUNT() مشکل میشود.
راه دیگر برای به دست آوردن ساختار یک زیردرخت از لیست مجاورت برای یک برنامه، این است که سطرهای مورد نظر خود را از مجموعه بازیابی نموده و سلسهمراتب مورد نظر را در حافظه بازیابی نماییم و از آن به عنوان درخت استفاده نماییم:
SELECT * FROM Comments WHERE bug_id = 1234;
INSERT INTO Comments (bug_id, parent_id, author, comment) VALUES (1234, 7, 'Kukla' , 'Thanks!' );
UPDATE Comments SET parent_id = 3 WHERE comment_id = 6;
SELECT parent_id FROM Comments WHERE comment_id = 6; -- returns 4 UPDATE Comments SET parent_id = 4 WHERE parent_id = 6; DELETE FROM Comments WHERE comment_id = 6;
- چه تعداد سطح برای پشتیبانی در درخت نیاز خواهیم داشت؟
- من همیشه از کار با کدی که ساختار دادهی درختی را مدیریت میکند، میترسم
- من باید اسکریپتی را به طور دورهای اجرا نمایم تا سطرهای یتیم موجود در درخت را حذف کند.
WITH CommentTree (comment_id, bug_id, parent_id, author, comment, depth) AS ( SELECT *, 0 AS depth FROM Comments WHERE parent_id IS NULL UNION ALL SELECT c.*, ct.depth+1 AS depth FROM CommentTreect JOIN Comments c ON (ct.comment_id = c.parent_id) ) SELECT * FROM CommentTree WHERE bug_id = 1234;
SELECT * FROM Comments START WITH comment_id = 9876 CONNECT BY PRIOR parent_id = comment_id;
CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY, path VARCHAR(1000), bug_id BIGINT UNSIGNED NOT NULL, author BIGINT UNSIGNED NOT NULL, comment_date DATETIME NOT NULL, comment TEXT NOT NULL, FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id), FOREIGN KEY (author) REFERENCES Accounts(account_id)
SELECT * FROM Comments AS c WHERE '1/4/6/7/' LIKE c.path || '%' ;
SELECT * FROM Comments AS c WHERE c.path LIKE '1/4/' || '%' ;
CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY, nsleft INTEGER NOT NULL, nsright INTEGER NOT NULL, bug_id BIGINT UNSIGNED NOT NULL, author BIGINT UNSIGNED NOT NULL, comment_date DATETIME NOT NULL, comment TEXT NOT NULL, FOREIGN KEY (bug_id) REFERENCES Bugs (bug_id), FOREIGN KEY (author) REFERENCES Accounts(account_id) );
شمارهی سمت چپ یک گره از تمام شمارههای سمت چپ فرزندان آن گره کوچکتر و شمارهی سمت راست آن گره از تمام شمارههای سمت راست آن گره بزرگتر است. این شمارهها هیچ ارتباطی به comment_id مربوط به آن گره ندارند.
یک راه حل ساده برای تخصیص این شمارهها به گرهها این است که از سمت چپ یک گره آغاز میکنیم و اولین شماره را اختصاص میدهیم و به همین به گرهای سمت چپ فرزندان میآییم و شمارهها را به صورت افزایشی به سمت چپ آنها نیز اختصاص میدهیم. سپس در ادامه به سمت راست آخرین نود رفته و از آن جا به سمت بالا میآییم و به همین ترتیب به صورت بازگشتی تخصیص شمارهها را ادامه میدهیم.
با اختصتص شمارهها به هر گره، میتوان از آنها برای یافتن نیاکان و فرزندان آن گره بهره جست. برای مثال برای بازیابی گرهی 4 و فرزندان (نوههای) آن باید دنبال گرههایی باشیم که شمارههای آن گرهها بین nsleft و nsright گرهی شماره4 باشد:
SELECT c2.* FROM Comments AS c1 JOIN Comments as c2 ON c2.nsleft BETWEEN c1.nsleft AND c1.nsright WHERE c1.comment_id = 4;
SELECT c2.* FROM Comments AS c1 JOIN Comment AS c2 ON c1.nsleft BETWEEN c2.nsleft AND c2.nsright WHERE c1.comment_id = 6;
SELECT parent.* FROM Comment AS c JOIN Comment AS parent ON c.nsleft BETWEEN parent.nsleft AND parent.nsright LEFT OUTER JOIN Comment AS in_between ON c.nsleft BETWEEN in_between.nsleft AND in_between.nsright AND in_between.nsleft BETWEEN parent.nsleft AND parent.nsright WHERE c.comment_id = 6 AND in_between.comment_id IS NULL;
-- make space for NS values 8 and 9 UPDATE Comment SET nsleft = CASE WHEN nsleft >= 8 THEN nsleft+2 ELSE nsleft END, nsright = nsright+2 WHERE nsright >= 7; -- create new child of comment #5, occupying NS values 8 and 9 INSERT INTO Comment (nsleft, nsright, author, comment) VALUES (8, 9, 'Fran' , 'Me too!' );
CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY, bug_id BIGINT UNSIGNED NOT NULL, author BIGINT UNSIGNED NOT NULL, comment_date DATETIME NOT NULL, comment TEXT NOT NULL, FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id), FOREIGN KEY (author) REFERENCES Accounts(account_id) ); CREATE TABLE TreePaths ( ancestor BIGINT UNSIGNED NOT NULL, descendant BIGINT UNSIGNED NOT NULL, PRIMARY KEY(ancestor, descendant), FOREIGN KEY (ancestor) REFERENCES Comments(comment_id), FOREIGN KEY (descendant) REFERENCES Comments(comment_id) );
به جای استفاده از جدول Comments برای ذخیرهی اطلاعات مربوط به یک درخت از جدول TreePath استفاده میکنیم. به ازای هر یک جفت گره در این درخت یک سطر در جدول ذخیره میشود که ارتباط پدر فرزندی را نمایش میدهد و الزاما نباید این دو پدر فرزند بلافصل باشد. همچنین یک سطر هم به ازای ارتباط هر گره با خودش به جدول اضافه میگردد.
پرسوجوهای بازیابی نیاکان و فرزندان (گرهها) از طریق جدول TreePaths سادهتر از روش مجموعههای تودرتو است. مثلا برای بازیابی فرزندان (نوههای) گرهی شمارهی 4، سطرهایی که نیاکان آنها 4 است را به دست میآوریم:
SELECT c.* FROM Comments AS c JOIN TreePaths AS t ON c.comment_id = t.descendant WHERE t.ancestor = 4;
SELECT c.* FROM Comments AS c JOIN TreePaths AS t ON c.comment_id = t.ancestor WHERE t.descendant = 6;
INSERT INTO TreePaths (ancestor, descendant) SELECT t.ancestor, 8 FROM TreePaths AS t WHERE t.descendant = 5 UNION ALL SELECT 8, 8;
DELETE FROM TreePaths WHERE descendant = 7;
DELETE FROM TreePaths WHERE descendant IN (SELECT descendant FROM TreePaths WHERE ancestor = 4);
DELETE FROM TreePaths WHERE descendant IN (SELECT descendant FROM TreePaths WHERE ancestor = 6) AND ancestor IN (SELECT ancestor FROM TreePaths WHERE descendant = 6 AND ancestor != descendant);
INSERT INTO TreePaths (ancestor, descendant) SELECT supertree.ancestor, subtree.descendant FROM TreePaths AS supertree CROSS JOIN TreePaths AS subtree WHERE supertree.descendant = 3 AND subtree.ancestor = 6;
میتوان عملکرد Closure Table را برای ایجاد پرسوجو روی فرزندان و پدر بلافصل را آسانتر نیز نمود. اگر فیلد path_length را به جدول TreePaths اضافه نماییم این کار انجام میشود. path_length گرهای که به خودش ارجاع میشود، صفر است. path_length فرزند بلافصل هر گره 1، path_length نوهی آن 2 میباشد و به همین ترتیب path_lengthها را در هر سطر مقداردهی میکنیم. اکنون یا فتن فرزند گرهی شمارهی 4 آسانتر است:
SELECT * FROM TreePaths WHERE ancestor = 4 AND path_length = 1;