عنوان:

‫استفاده از روال‌های مشترک ولی موردی در جداول


نویسنده: هادی مزارعی
تاریخ: ۱۴۰۴/۰۸/۰۳ ۲۰:۳۰
آدرس: www.dntips.ir
فرض کنیم پروژه‌ای شامل 100 جدول است و هرجدول حدود 1000 ردیف دارد و تمامی اطلاعاتی که در جدوال ثبت می‌شوند بدلیل ماهیت اون کسب و کار ممکن است به ازای هر ردیف توضیحاتی توسط کاربر در آن درج شود. این ستون می‌تواند یک نام مانند Comment و یا Remark داشته باشد.

سوال:
اگر بجای ایجاد ستون Comment و یا Remark در تمامی جدوال که احتمالا 90 درصد ردیف‌ها نیازی به آن ندارند و در تمامی جداول null باقی خواهند ماند، یک جدول با نام Comment ایجاد کنم و ستون‌های آن بصورت زیر باشد:

Id, TableName, TableId, Comment

اگرچه که امکان ایجاد رابطه با تمام جدوال برای TableId وجود ندارد اما در زمان واکشی داده‌ها می‌توان در مدل خروجی (Output Model) یک List از Commentهای متناظر با جدول واکشی شده قرار داد تا برنامه سمت Client از آن استفاده کند. آیا پیاده سازی این روش اشکال فنی دارد؟

نظرات

  • محسن میرشاهرضا در ۱۴۰۴/۰۸/۰۳ ۲۱:۵۸
    یک جدول که همه کامنتها رو بازای همه جداول نگهداری میکنه....
    باید ببینید شرایط دقیقا چیه اگر نگران تعداد زیاد نال هستید ( که احیانا مشکل فضای ذخیره سازی و شرینک و .. ایجاد کنه ) در MsSql Server راه حل داره. میتونید به دیتابیس بگین در این ستون تعداد زیادی نال خواهم داشت و خودش این به بهترین روش مدیریت میکنه. اگر تعداد رکوردها کمه هر کدوم از این طراحی ها به نظرم اشکال ندارن گرچه دومی بدون اینکه مزیتی بده یک رابطه دیتابیسی ایجاد میکنه. یعنی برای نمایش اطلاعات باید از جویین یا یه تریک دیگه استفاده کنیم یعنی کار اضافه . با اینکه زیاد نیست ولی به هر کاره بهتره نکنیم اینکار رو. ولی اگر تعداد رکوردها زیاد باشه و تعداد تراکنشها هم زیاد باشه اینکار حتما بده. برای بهینه سازی نیاز به ایندکس اضافه پیدا میکنیم و....
    حتی در بعضی سناریوها برای حفظ پرفورمنس داده دینرمال هم نگه میداریم.
    کلا برای اضافه کاری باید دلیل قانع کننده داشته باشیم
    • هادی مزارعی در ۱۴۰۴/۰۸/۰۳ ۲۲:۲۳
      یکی از دلایل اصلی، امکان درج چندین توضیح برای یک ردیف است. این توضیحات به همراه اطلاعاتی مانند نام کاربری که توضیح را ثبت کرده به همراه زمان ثبت، برای کلاینت ارسال میشه و قراره که همان کاربر بتونه توضیح را اصلاح و یا حذف کنه. اگر بخوام به ازای هر جدول یک جدول Comment جداگانه بسازم (البته راه حل دیگه‌ای بلد نیستم) باید دقیقا 100 جدول دیگه به بانک اضافه کنم: TableA, TableAComments و TableB, TableBComments و... ولی پیاده سازی یک جدول سراسری برای جمع آوری Commentهای یک پروژه ضمن اینکه تعداد جداول و کلاس‌های تولید شده در پروژه را بشدت کاهش میده، مدیریت این ویژگی از نظر پیاده سازی هم برام ساده‌تر میشه و تغییرات تنها برای یک جدول و Handlerهای مربوط به آن اعمال خواهد شد.
      • وحید نصیری در ۱۴۰۴/۰۸/۰۳ ۲۳:۵۲
        روش پیشنهادی شما برای ایجاد یک جدول مرکزی به نام Comment (با ستون‌هایی مانند Id, TableName, TableId, Comment) یک روش رایج و معتبر در طراحی بانک‌های اطلاعاتی است. این الگو اغلب به عنوان "Polymorphic Associations" یا "Generic Foreign Keys" شناخته می‌شود و در فریم‌ورک‌هایی مانند Entity Framework پشتیبانی می‌شود. از مزایای فنی این روش، کاهش پیچیدگی و تعداد جداول است؛ همان‌طور که گفتید، به جای ایجاد ۱۰۰ جدول جداگانه برای کامنت‌ها (مثل TableAComments، TableBComments و غیره)، فقط یک جدول دارید. هرچند این روش مشکلی اساسی ندارد، اما چند نکته فنی باید مدیریت شود تا از مشکلات آینده جلوگیری کنید: عدم امکان رابطه خارجی (Foreign Key) مستقیم: چون TableId به جداول مختلف اشاره می‌کند، نمی‌توانید FK تعریف کنید. این یعنی: Integrity داده‌ها توسط دیتابیس چک نمی‌شود. اگر ردیفی از جدول اصلی حذف شود، کامنت‌های مربوطه orphaned (یتیم) می‌مانند. راه‌حل: این را در سطح اپلیکیشن مدیریت کنید. مثلاً قبل از حذف یک رکورد از جدول اصلی، کامنت‌های مرتبط را چک و حذف/به‌روزرسانی کنید (با استفاده از transactionها برای atomicity). جایگزین‌های دیگر: اگر بخواهید رابطه خارجی داشته باشید، می‌توانید از inheritance در دیتابیس (مثل Table-Per-Type در EF) استفاده کنید، اما برای ۱۰۰ جدول پیچیده است.

        یک نمونه پیاده سازی آن با EF-Core که معمولاً با استفاده از رابطه چندشکلی (Polymorphic Association) که توسط برنامه مدیریت می‌شود (و نه به صورت داخلی توسط Foreign Key):
        فرض کنید دو جدول از ۱۰۰ جدول شما Product و Order باشند. این مدل‌ها به طور معمول تعریف می‌شوند و نباید شامل ستون Comment باشند.
        // مدل جدول Product
        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public decimal Price { get; set; }
            
            // (اختیاری) یک Navigation Property برای دسترسی آسان به کامنت‌ها
            public ICollection<Comment> Comments { get; set; } = new List<Comment>();
        }
        
        // مدل جدول Order
        public class Order
        {
            public int Id { get; set; }
            public DateTime OrderDate { get; set; }
            public int CustomerId { get; set; }
            
            // (اختیاری) یک Navigation Property برای دسترسی آسان به کامنت‌ها
            public ICollection<Comment> Comments { get; set; } = new List<Comment>();
        }

        مدل مرکزی Comment
        این مدل هسته اصلی طراحی شماست. شامل ستون‌هایی است که مرجع ردیف اصلی را نگه می‌دارند.
        public class Comment
        {
            public int Id { get; set; }
            
            // ۱. کلید اصلی ردیفی که کامنت برای آن ثبت شده (TableId در طرح شما)
            public int EntityId { get; set; } 
        
            // ۲. نوع موجودیت (TableName در طرح شما) 
            // بهتر است از یک Enum یا رشته (string) برای نگهداری نام نوع استفاده شود.
            // در اینجا از رشته برای سادگی استفاده شده است.
            public string EntityType { get; set; } 
        
            // محتوای کامنت
            public string Content { get; set; } 
            
            // اطلاعات ثبت
            public int UserId { get; set; }
            public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        
            // (اختیاری) Navigation Property برای کاربر ثبت کننده
            // این یک Foreign Key سنتی و واقعی است.
            public User CreatedBy { get; set; }
        }

        پیکربندی (Configuration) در DbContext
        در متد OnModelCreating کلاس DbContext، شما باید EF Core را پیکربندی کنید. از آنجا که نمی‌توانید یک Foreign Key سنتی بین Comment.EntityId و 100 جدول دیگر ایجاد کنید، از روش‌هایی برای تعریف روابط یک‌طرفه یا استفاده از Property Access و Query Filters استفاده می‌شود.

        تعریف Navigation Properties
        در این الگو، معمولاً رابطه یک‌طرفه (One-Way Relationship) از Comment به Product یا Order تعریف نمی‌شود زیرا EF Core نمی‌تواند آن را به صورت سنتی تشخیص دهد. اما می‌توان رابطه‌های دو‌طرفه مصنوعی (Artificial Two-Way) را با استفاده از فیلتر در پرس‌وجوها (Queries) مدیریت کرد.

        پیکربندی کلیدها و فیلترها (Query Filters)
        public class AppDbContext : DbContext
        {
            public DbSet<Product> Products { get; set; }
            public DbSet<Order> Orders { get; set; }
            public DbSet<Comment> Comments { get; set; }
            
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);
        
                // ۱. تعریف کلید اصلی مرکب غیرسنتی
                // اگرچه EF Core از لحاظ فنی یک PK مرکب (EntityType, EntityId) نمی‌خواهد، 
                // اما از لحاظ منطقی برای جستجوی سریع به آن نیاز دارید (با ایندکس).
                
                // ۲. تعریف ایندکس حیاتی: این مهمترین بخش برای عملکرد است.
                modelBuilder.Entity<Comment>()
                    .HasIndex(c => new { c.EntityType, c.EntityId })
                    .IsUnique(false); // Index باید غیر یکتا باشد
        
                // ۳. حذف Navigation Properties (اختیاری)
                // برای جلوگیری از خطا، باید به EF Core بگویید که Navigation Properties 
                // در مدل‌های Product و Order را نادیده بگیرد.
                modelBuilder.Entity<Product>()
                    .Ignore(p => p.Comments);
                    
                modelBuilder.Entity<Order>()
                    .Ignore(o => o.Comments);
                
                // توجه: اگرچه در بالا Ignore کردیم، اما می‌توانیم در صورت نیاز از 
                // Shadow Properties و TPH/TPT برای تعریف این روابط استفاده کنیم که پیچیده‌تر است.
            }
        }

        نحوه واکشی (Data Retrieval)
        واکشی داده‌ها توسط منطق برنامه (Business Logic) یا Repository Pattern انجام می‌شود. شما نمی‌توانید از متد Include() استاندارد EF Core برای بارگذاری خودکار کامنت‌ها استفاده کنید، بلکه باید از یک فیلتر صریح استفاده کنید:
        public class DataService
        {
            private readonly AppDbContext _context;
        
            public DataService(AppDbContext context)
            {
                _context = context;
            }
        
            public async Task<ProductOutputModel> GetProductWithCommentsAsync(int productId)
            {
                // مرحله ۱: واکشی ردیف اصلی
                var product = await _context.Products.FindAsync(productId);
                if (product == null) return null;
        
                // مرحله ۲: واکشی کامنت‌های مرتبط با استفاده از فیلتر
                var comments = await _context.Comments
                    .Where(c => c.EntityType == nameof(Product) && c.EntityId == productId)
                    .OrderByDescending(c => c.CreatedAt)
                    // (اختیاری) اگر نیاز به اطلاعات کاربر ثبت کننده است:
                    .Include(c => c.CreatedBy) 
                    .ToListAsync();
        
                // مرحله ۳: نگاشت به مدل خروجی (Output Model)
                var outputModel = new ProductOutputModel 
                {
                    Id = product.Id,
                    Name = product.Name,
                    Comments = comments.Select(c => new CommentDto { /* نگاشت ویژگی‌ها */ }).ToList()
                };
        
                return outputModel;
            }
        }
        این روش به شما امکان می‌دهد با تنها یک جدول، امکان درج توضیحات متعدد با اطلاعات هویتی و زمانی برای ۱۰۰ جدول پروژه خود را فراهم کنید و پیچیدگی پیاده‌سازی و نگهداری را به شدت کاهش دهید. کلید موفقیت، ایندکس‌گذاری صحیح بر روی ستون‌های (EntityType, EntityId) است تا واکشی کامنت‌ها سریع باقی بماند.