Məlumatların databazaya yazılması, oxunması, dəyişdirilməsi və silinməsi demək olar ki bütün layihələrdə olur. Bu əməliyyatlara CRUD əməliyyatları deyilir. Bəs CRUD əməliyyatlarını yerinə yetirən funksiyaları hansı formada yazmaq lazımdır?. Bu postda mən sizə CRUD əməliyyatlarını yerinə yetirilməsinin ən yaxşı üsullarından biri olan Repository və UnitOfWork Pattern haqqında yazmışam. Bu postu oxuduqdan sonra Repository və UnitOfWork Pattern nədir?, Bizə hansı üstünlüklər verir və s.  suallara cavab tapacaqsız.

Qeyd: Mən terminlərin çoxsunu elə olduğu kimi yazıram. Çünki azərbaycanca yazdıqda mənasın itirə bilir.

Proqramlaşdırmada öyrənməyin ən yaxşı üsulu problemi anlamaqdır. Gəlin əvvəlcə problemi araşdıraq.  İlk öncə aşağıdakı kod bloka baxaq.

public class PersonController : ApiController
    {
        private readonly AppDbContext _appDbContext= new AppDbContext();

        [HttpGet]
        public IHttpActionResult Index()
        {
            return Ok(_appDbContext.Persons.ToList());
        }

        [Route(Name = "GetPersonById")]
        [HttpGet]
        public IHttpActionResult Get(int id)
        {
            var result = _appDbContext.Persons.FirstOrDefault(x => x.Id == id);

            if (result == null)
                return NotFound();

            return Ok(result);
        }
       
        [HttpPost]
        public IHttpActionResult Post(Person person)
        {
            if (ModelState.IsValid)
            {
                _appDbContext.Persons.Add(person);
                _appDbContext.SaveChanges();
            }
            return Created("GetPersonById", person);
        }

        [HttpPut]
        public IHttpActionResult Edit(Person person)
        {
            if (person.Id == default(int))
                return BadRequest();

            if (ModelState.IsValid)
            {
                _appDbContext.Persons.Attach(person);
                _appDbContext.Entry<Person>(person).State = EntityState.Modified;
                _appDbContext.SaveChanges();
            }

            return Created("GetPersonById", person);
        }

        protected override void Dispose(bool disposing)
        {
            _appDbContext.Dispose();
            base.Dispose(disposing);
        }
    }

AppDbContext obyekti Controller -in icərisində yaradılıb və hər action metodun icərisində özünə uyğun baza əməliyyatı edilir. İlk baxışdan mükəmməl bir kod kimi görünür. Kodun funksionallığı da çox gözəl işləyəcək. Sizcə burada nə problem var?

Qeyd: Mən burada EntityFramework(ORM) istifadə etmişəm. Amma bu o qədərdə vacib deyil. Yəni siz orada standart Connection, Command da istifadə edə bilərsiz.

Bu şəkildə kodun yazılması aşağıdakı problemlər yaradır. 

  1. Burada ORM -in classları bir başa Controller -in içərisində yazıldığına görə Controller bir başa həmin ORM -dən asılıdır. Əgər gələcəkdə siz həmin ORM -i dəyişib başqa birini istifadə etmək istəsəz çox yerdə dəyişiklik etməli olacaqsız.
  2. Controller ORM -dən asılı olduğu üçün Contorller metodların Unit Test edən zaman böyük çətinliklərlə qarşılaşacaqsız.
  3. Siz proqramın UI tərəfini dəyişdirmək istəsəz və ya həmin prqramın mobil proqramını hazırlamalı olsanız, o kodları yenidən yazmalı olacaqsınız.
  4. Ayrı-ayrı layerlər aid olan kodlar qarışıb bir birinə. Bunu spagettiyə bənzədə bilərik. Bunları ayırmaq lazımdır.

Bu problemləri həll etmək üçün ən yaxşı yanaşmalardan biri Repository və UnitOfWork Pattern -dir. Repository təməl olarak databaza sorğulama əməliyyatlarını bir mərkəzden aparlımasını təmin edərək proqramın məntiq hissəsində bu kodların yazılmasının aradan qaldırır və bu şəkildə sorğu ve kod təkrarlarının yazılmasının qarşısını alır. Yəni sizə lazım olan baza əməliyyatını təkrar təkrar proqramın məntiq kodlarının icərisinə yazmaq əvəzinə uyğun Repository -nin içərisinə yazaraq lazım olan bütün yerlərdə çağırmaq lazımdır. Bunun bizə digər bir üstünlüyü də proqramın məntiq kodları üçün asan Unit Test metodları yaza bilməyimizdir. Proqramın məntiq tərəfi üçün vacib deyil Repository arxa tərəfdə hansı ORM dən istifadə edir. Onun üçün vacib olan məlumatın gəlməsi və getməsidir.

Repository -in yaradılması

Deməli biz bir class yaratmalıyıq və Controller -də yazdığımız baza əməliyyatlarını onun icərisində etməliyik. PersonRepository adlı class yaradaq və Controller -də yazdığımız databaza əməliyyatları bu classın içərisinə yazaq.

 public class PersonRepository : IDisposable
    {
        private AppDbContext appDbContext = new AppDbContext();
 
        public IEnumerable<Person> GetAll()
        {
            return appDbContext.Persons.ToList();
        }
 
        public Person Get(int id)
        {
            return appDbContext.Persons.FirstOrDefault(x => x.Id == id);
        }
 
        public void Add(Person person)
        {
            appDbContext.Persons.Add(person);
            appDbContext.SaveChanges();
        }
 
        public void Update(Person person)
        {
            appDbContext.Persons.Attach(person);
            appDbContext.Entry(person).State = EntityState.Modified;
            appDbContext.SaveChanges();
        }
 
        public void Delete(Person person)
        {
            appDbContext.Persons.Remove(person);
            appDbContext.SaveChanges();
        }
 
        public void Dispose()
        {
            appDbContext.Dispose();
        }
    }

Burada da balaca bir dəyişiklik edərək kodu daha genişlənə bilən edə bilərik. Belə ki biz ümumi Repository interface -i hazırlaya bilərik və yuxarıdaki Repository -imizi həmin bu interface -dən implement edə bilərik. Belə olan halda biz gələcəkdə yuxarıdakı Repository -nin başqa versiyalarını və ya başqa ORM -dən istifadə olunan verisyalarını da yarada bilərik. Contorller -in içərisində isə yalnız bu interface -dən istifadə edə bilərik. Bu zaman bizim Contorller -imiz hər hansı bir implementation olan Repository -dən asılı qalmaz. Həmin Repository -inin obyektini yaradan zaman yeni versiyanı yaradacağıq və digər heç bir yerdə dəyişiklik etmədən bizim kodumuz əvvəl olduğu kimi işləyəcək.

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate = null);
    TEntity Get(Expression<Func<TEntity, bool>> predicate);
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(TEntity entity);
}

Yuraxıdakı interface -ə standart metodlar yazmışam. Real layihələrdə əlavə bir neçə metod da yazıram. Bu interface -in implementation -ı olan class CRUD əməliyyatın yerinə yetirən məntiqi tutacaq.

public class PersonRepositoryNew : IRepository<Person>, IDisposable
    {
        private AppDbContext appDbContext = new AppDbContext();
 
        public IEnumerable<Person> GetAll(Expression<Func<Person, bool>> predicate = null)
        {
            return appDbContext.Persons.Where(predicate).ToList();
        }
 
        public Person Get(Expression<Func<Person, bool>> predicate)
        {
            return appDbContext.Persons.FirstOrDefault(predicate);
        }
 
        public void Add(Person entity)
        {
            appDbContext.Persons.Add(entity);
            appDbContext.SaveChanges();
        }
 
        public void Update(Person entity)
        {
            appDbContext.Persons.Attach(entity);
            appDbContext.Entry(entity).State = EntityState.Modified;
            appDbContext.SaveChanges();
        }
 
        public void Delete(Person entity)
        {
            appDbContext.Persons.Remove(entity);
            appDbContext.SaveChanges();
        }
 
        public void Dispose()
        {
            appDbContext.Dispose();
        }
    }

İndi isə yuxarıdakı metodları Controller -də çağıraq

 public class NewPersonController : ApiController
    {
        private readonly NewPersonRepository _personRepository = new NewPersonRepository();
 
        [HttpGet]
        public IHttpActionResult Index()
        {
            return Ok(_personRepository.GetAll());
        }
 
        [Route(Name = "GetPersonById")]
        [HttpGet]
        public IHttpActionResult Get(int id)
        {
            var result = _personRepository.Get(x => x.Id == id);
 
            if (result == null)
                return NotFound();
 
            return Ok(result);
        }
 
        [HttpPost]
        public IHttpActionResult Post(Person person)
        {
            if (ModelState.IsValid)
            {
                _personRepository.Add(person);
            }
            return Created("GetPersonById", person);
        }
 
        [HttpPut]
        public IHttpActionResult Edit(Person person)
        {
            if (person.Id == default(int))
                return BadRequest();
 
            if (ModelState.IsValid)
            {
                _personRepository.Update(person);
            }
 
            return Created("GetPersonById", person);
        }
 
        protected override void Dispose(bool disposing)
        {
            _personRepository.Dispose();
            base.Dispose(disposing);
        }
    }

UnitOfWork

İndiyə qədər hər şey çox gözəl gedir. Amma yuxarıdakı Repository -də bir problem var. Bizim hal-hazırda bir Repository -imiz var amma real layihələrdə birdən çox Repository olur. Bizim AppDbContext in obyektini Repository -nin içerisində yaratmağimiz düzgün deyil ve SaveChanges metodun Repository -nin içərsində olmağı o deməkdir ki, hər dəfə Add, Update və ya Delete metodun çağırılanda SaveChanges metodu işləyəcək. Bu isə bir birindən aslı olan məlumatların birinin databazada saxlanıb birinin saxlanmama şansını artirir və məlumatın bazada yarımçiq olmasina gətrib çıxara bilər. Məsələn deyək ki, bizim şəxsi məlumatları saxlayan Persons və həmin şəxslərin ünvan məlumatlarını saxlayan PersonAddresses adlı iki cədvəlimiz var və bizə deyiblər ki, Persons cədvəlində saxlanılan bütün şəxslərin ən azı bir ünvan məlumatı olmalıdır. Biz də bu cədvəllərin hər biri üçün yuxarıdakı kimi Repository hazırlamışıq. Bu Repository -lərə görə biz şəxs və ünvan məlumatlarını tək-tək databazaya göndərəcəyik. Çünki bizim SaveChanges metodumuz Add metodu işləyən zaman işləyir. Buda belə bir risk yaradır. Şəxsi məlumatlar saxladıqdan sonra ünvan məlumatların saxlayan zaman şəbəkə itsə və yaxud digər nəsə xəta çıxsa ünvan məlumatı databazaya göndərə bilməyəcəyik. Bu isə databazada yarımçıq məlumatların olmasına gətirib çıxaracaq. Deməli biz bu baza əməliyyatı eyni anda databazaya göndərməliyik. Bunun üçün SaveChanges methodunun Repository -inin içerisindən çixarıb başqa bir classin içərisində yazamaq lazımdır. Həmin bu class UnitOfWork adlanır.

public class UnitOfWork : IDisposable
    {
        private readonly AppDbContext _appDbContext= new AppDbContext();

        public UnitOfWork()
        {
            PersonRepository = new NewPersonRepository(_appDbContext);
        }

        public NewPersonRepository PersonRepository;

        public void SaveChanges()
        {
            _appDbContext.SaveChanges();
        }

        public void Dispose()
        {
             _appDbContext.Dispose();
        }
    }

Burada AppDbContext -iUnitOfWork -in içərsində yaradılmalıdır və hər bir Repository -inin qurucusuna(constructor) parametr kimi ötürülməlidir. Heç bir Repository-nin SaveChanges metodu olmamalıdır və  Repository-lərin heçbir metodu AppDbContext -in SaveChanges metodunu çağırmamalıdır. Layihədə bir neçə proqramçı işləyən zaman və yaxud gələcəkdə başqa proqramçı layihəyə baxan zaman səhvən AppDbContext-in SaveChanges metodun Repository-nin içərisində çağırmasının qarşısını almaq üçün AppDbContext -in SaveChanges metodu olmayan İnterface -ni yaradıb Repositorylərə onu göndərmək lazımdır.

public class UowPersonController : ApiController
    {
        private readonly UnitOfWork _unitOfWork = new UnitOfWork();

        [HttpGet]
        public IHttpActionResult Index()
        {
            return Ok(_unitOfWork.PersonRepository.GetAll());
        }

        [Route(Name = "GetPersonById")]
        [HttpGet]
        public IHttpActionResult Get(int id)
        {
            var result = _unitOfWork.PersonRepository.Get(x => x.Id == id);

            if (result == null)
                return NotFound();

            return Ok(result);
        }

        [HttpPost]
        public IHttpActionResult Post(Person person)
        {
            if (ModelState.IsValid)
            {
                _unitOfWork.PersonRepository.Add(person);
                _unitOfWork.SaveChanges();
            }
            return Created("GetPersonById", person);
        }

        [HttpPut]
        public IHttpActionResult Edit(Person person)
        {
            if (person.Id == default(int))
                return BadRequest();

            if (ModelState.IsValid)
            {
                _unitOfWork.PersonRepository.Update(person);
                _unitOfWork.SaveChanges();
            }

            return Created("GetPersonById", person);
        }

        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }

Bəzi məqalələrdə Repository və UnitOfWork Pattern -in lazım olmadığını, EntityFramework bunu edib tərar bunu etməyə ehtiyac qalmadığını oxumuşam. DbSet Repository, DbContex -in UnitofWork olduğunu deyirlər. Düzdür demək olar ki eynisidir amma burada başqa bir şey var. Əgər biz DbContext -i Controller -də istifadə etsək bizim proqramımız bir başa EntityFramewordən(və ya başqa ORM) yəni Persistance Framework -dən asılı olacaq.

Ümumi(Generic) Repository

Böyük layihərin databazalarında yüzdən artıq cədvəl olur. Belə olan halda biz hər biri üçün bir Repository hazırlamalıyıqmı?. Əgər siz EntityFramework istifadə edirsizsə aşağıdakı kimi ümumi bir Repositoy hazırlaya bilərsiz.

 public class GenericRepository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private readonly DbSet<TEntity> _dbset;
        private readonly AppDbContext _appDbContext;

        public GenericRepository(AppDbContext appDbContext)
        {
            _dbset = appDbContext.Set<TEntity>();
            _appDbContext= appDbContext;
        }

        public void Add(TEntity entity)
        {
            _dbset.Add(entity);
        }

        public void Update(TEntity entity)
        {
            _dbset.Attach(entity);
            _appDbContext.Entry(entity).State = EntityState.Modified;
        }

        public void Delete(TEntity entity)
        {
            _dbset.Remove(entity);
        }

        public TEntity Get(Expression<Func<TEntity, bool>> predicate)
        {
           return _dbset.FirstOrDefault(predicate);
        }

        public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate = null)
        {
           return _dbset.Where(predicate).ToList();
        }
    }

Ümumi Repository-ə uyğun olaraq UnitOfWork -də dəyişiklik edək.

 public class UnitOfWork : IDisposable
    {
        private readonly AppDbContext _appDbContext= new AppDbContext();
       
        public Dictionary<Type, object> _repositories = new Dictionary<Type, object>();

        public IRepository<T> Repository<T>() where T : class
        {
            if (_repositories.Keys.Contains(typeof(T)) == true)
            {
                return _repositories[typeof(T)] as IRepository<T>;
            }
            IRepository<T> repo = new GenericRepository<T>(_appDbContext);
            _repositories.Add(typeof(T), repo);
            return repo;
        }

        public void SaveChanges()
        {
            _appDbContext.SaveChanges();
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }

Və bunun Controller -də istifadəsini yazaq.

 public class UowPersonController : ApiController
    {
        private readonly UnitOfWork _unitOfWork = new UnitOfWork();

        [HttpGet]
        public IHttpActionResult Index()
        {
            return Ok(_unitOfWork.Repository<Person>().GetAll());
        }

        [Route(Name = "GetPersonById")]
        [HttpGet]
        public IHttpActionResult Get(int id)
        {
            var result = _unitOfWork.Repository<Person>().Get(x => x.Id == id);

            if (result == null)
                return NotFound();

            return Ok(result);
        }

        [HttpPost]
        public IHttpActionResult Post(Person person)
        {
            if (ModelState.IsValid)
            {
                _unitOfWork.Repository<Person>().Add(person);
                _unitOfWork.SaveChanges();
            }
            return Created("GetPersonById", person);
        }

        [HttpPut]
        public IHttpActionResult Edit(Person person)
        {
            if (person.Id == default(int))
                return BadRequest();

            if (ModelState.IsValid)
            {
                _unitOfWork.Repository<Person>().Update(person);
                _unitOfWork.SaveChanges();
            }

            return Created("GetPersonById", person);
        }

        protected override void Dispose(bool disposing)
        {
            _unitOfWork.Dispose();
            base.Dispose(disposing);
        }
    }

Qeyd: Conrollerin içərisində servis classların obyektini yaradırsınızsa həmin Controller həmin o servis classından asılı olur. Controller -in daxilində obyektini yaratdığınız servis classların obyektini Controller -in constructor-na ötürməyiniz daha yaxşı olar. Məsələn UnitOfWork -in interface-ni yaratmaq və Controller-in həmin interface tipində parametri olan constructor hazırlamaq lazımdır. Yəni asılılığ aradan qaldırmaq lazımdır. Controller -ə arqument kimi UnitOfWork classın obyektini ötürmək üçün IoC (Inversion of Control) Container istifadə edilməlidir.

Ümumi Repository istifadə edən zaman bir problemlə qarşılaşacaqsınız. Ümumi Repository yazaraq özünüz təkrar Repository hazırlamaqdan canınızı qurtarırsız amma kodları təkrar yazmalı olacaqsız. Deyək ki siz bir neçə yerdə şəxsi məlumatları həmin şəxlərin ünvan məlumatları ilə birlikdə databazadan gətirmək istəyirsiz. Ümumi Repository yazan zaman harada sizə bu formada məlumat gətirmək lazımdırsa siz bu kodları təkrar-təkrar yazmalı olacaqsız. Ona görə də mən daha qarışıq bir yanaşma istifadə edirəm və demək olar ki, hazırladığım bütün yeni layihələrdə bunu tətbiq edirəm. Biz həm istəmirik çox Repository hazırlayaq həm də istəmirik təkrar kod yazaq. Bu zaman mən yanaşmaların hər ikisini tətbiq edirəm. Hanısların ki xüsusi özünə məxsus metodları olmalıdır onlar üçün xüsusi Repository hazırlayıram yox əgər xüsusi bir metod olmayacaqsa ümumi Repositoy-dən istifadə edirəm. Məsələn soraqça cədvəllər üçün demək olar ki Repository hazırlamıram. 

Mənim hazırladığım və bütün layihələrimdə istifadə etdiyim DataAccess kodlara buradan baxıb yükləyib və öz layihlərinizdə tətbiq edə bilərsiniz

Bir cavab yazın

Sizin e-poçt ünvanınız dərc edilməyəcəkdir. Gərəkli sahələr * ilə işarələnmişdir