Cześć!
W dwudziestym dziewiątym wpisie na blogu poruszę temat, od którego nie jeden programista zaczynał swoją programistyczną karierę. W tym gronie też jestem ja i pamiętam, jak w 2013 roku gorączkowo w C# zgłębiałem temat CRUD do portfolio. Wtedy zadawałem sobie tytułowe pytanie — czym jest CRUD?
Dziś podchodzę do tematu z uśmiechem na ustach, bo wiem jak dużą ekscytację miałem, gdy pierwszy raz coś udało mi się odczytać dane, zmodyfikować i usunąć je oraz dodać do zbioru.
No właśnie, zostawiam Ci kilka zagadnień na zachętę i lecimy:
- Czym jest CRUD?
- Jak wygląda architektura MVC na prostym przykładzie?
- W jaki sposób zaimplementować CRUD?
Odpalaj swoje IDE i zabieramy się do pracy.
Zatrzymaj się!
Książki to obowiązkowa pozycja dla każdego zainteresowanego programowaniem!
Jest to zdecydowanie jedno z najlepszych źródeł do nauki programowania! Zyskasz przewagę w branży IT i osiągniesz dużo jako deweloper.
Czym jest CRUD — Wprowadzenie
CRUD to akronim od słów Create, Read (Retrieve), Update, Delete (Destroy). Dotyczy on operacji, jakie programista wykonuje na zbiorze danych, czyli może być to plik tekstowy, baza danych czy struktura danych, której cykl życia zależy od cyklu życia uruchomionej aplikacji. Najczęściej jednak używany jest w kontekście pracy z danymi przechowywanymi w bazie danych.
Do tego oczywiście może być używany w protokole komunikacyjnym http. W tabeli poniżej rozpisałem metody SQL i te z protokołu HTTP do obsługi operacji CRUD.
Jeśli popatrzymy na CRUD bardziej szczegółowo, to będzie to wyglądało następująco:
- Create (stwórz) – tworzenie użytkownika, produktu, zamówienia, roli użytkownika czy faktury. Ten sam widok użyty będzie użyty do tworzenia oraz aktualizacji danych;
- Read (odczytaj) – odczytanie danych do widoku użytkownika, produktu, zamówienia, roli użytkownika czy faktury. Zazwyczaj wykorzystywany do podglądu danych lub do pobrania danych do edycji do formularza;
- Update (zaktualizuj) – aktualizacja danych użytkownika, produktu, zamówienia, roli użytkownika czy faktury, która wykorzystuje ten sam formularz co Create i dane pobierane są za pomocą operacji Read;
- Delete (usuwanie) – usunięcie użytkownika, produktu, zamówienia, roli użytkownika czy faktury. Zazwyczaj usunięcie nie jest fizycznym skasowaniem danych z tabeli. Polega to na zmianie flagi, która odpowiada za widoczność danych. Dla przykładu flaga Active, która w momencie usuwania zmienia swój stan z 1 na 0. Dane usunięte mogą być jednak używane dalej w funkcji Read przykładowo do raportów;
Każda z tych czterech operacji jest bazową funkcjonalnością aplikacji, jakich używamy na co dzień. Co ciekawe, wielkie systemy mają w sobie dziesiątki implementacji CRUD. Wyobraź sobie prosty sklep internetowy. Dodajemy tam produkty, składamy zamówienia, zapisujemy klientów, dodajemy rabaty. Wszystko to obsługuje osobna funkcjonalność, czyli CRUD.
CRUD jednak nie jest wszystkim, co taka aplikacja zawierać musi, ponieważ dochodzą walidacje, logi, testy jednostkowe i cała logika biznesowa składa się na kompletne rozwiązanie wdrażane na produkcji.
Czym jest CRUD — Implementacja
Czas pokazać CRUD od strony kodu, gdzie do implementacji używać będę języka C# oraz framework dotNET Core wraz z bazą danych MSSQL. Język nie jest tu istotny, najważniejsze dla mnie w tym momencie jest to, byś Ty ogarniał koncepcję. Polecam Ci zaimplementować CRUD w używanym przez Ciebie języku i wrzucił go sobie do portfolio. Będziesz mieć co pokazać na rozmowie o pracę, a na stanowisko juniora nie powinno zabraknąć zagadnień przedstawionych w dzisiejszym artykule.
Baza danych
Zacznę od stworzenia bazy danych i aby to zrobić, polecam zainstalować SQL Server Management Studio (SSMS) w dowolnej wersji, ale ja preferuję wersję v18. Następnie utworzę tabelę o nazwie Product, gdzie będę chciał dodawać, usuwać, modyfikować i odczytywać dane o produktach. W tabeli znajdą się następujące pola:
- ID;
- Name;
- Description;
- Code;
- Count;
- ProduceDate;
- Active;
Oczywiście może być ich więcej, jednak do zrozumienia zagadnienia tyle w zupełności wystarczy. Cały skrypt tworzący bazę danych oraz tabelę przedstawiam poniżej. Nie ma potrzeby zagłębiać się w szczegóły kodu SQL.
USE [master]
GO
CREATE DATABASE [CRUD]
CONTAINMENT = NONE
ON PRIMARY
( NAME = N'CRUD', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\CRUD.mdf' , SIZE = 8192KB , MAXSIZE = UNLIMITED, FILEGROWTH = 65536KB )
LOG ON
( NAME = N'CRUD_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\CRUD_log.ldf' , SIZE = 8192KB , MAXSIZE = 2048GB , FILEGROWTH = 65536KB )
WITH CATALOG_COLLATION = DATABASE_DEFAULT
GO
ALTER DATABASE [CRUD] SET COMPATIBILITY_LEVEL = 150
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [CRUD].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [CRUD] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [CRUD] SET ANSI_NULLS OFF
GO
ALTER DATABASE [CRUD] SET ANSI_PADDING OFF
GO
ALTER DATABASE [CRUD] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [CRUD] SET ARITHABORT OFF
GO
ALTER DATABASE [CRUD] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [CRUD] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [CRUD] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [CRUD] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [CRUD] SET CURSOR_DEFAULT GLOBAL
GO
ALTER DATABASE [CRUD] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [CRUD] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [CRUD] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [CRUD] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [CRUD] SET DISABLE_BROKER
GO
ALTER DATABASE [CRUD] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [CRUD] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [CRUD] SET TRUSTWORTHY OFF
GO
ALTER DATABASE [CRUD] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE [CRUD] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [CRUD] SET READ_COMMITTED_SNAPSHOT OFF
GO
ALTER DATABASE [CRUD] SET HONOR_BROKER_PRIORITY OFF
GO
ALTER DATABASE [CRUD] SET RECOVERY FULL
GO
ALTER DATABASE [CRUD] SET MULTI_USER
GO
ALTER DATABASE [CRUD] SET PAGE_VERIFY CHECKSUM
GO
ALTER DATABASE [CRUD] SET DB_CHAINING OFF
GO
ALTER DATABASE [CRUD] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF )
GO
ALTER DATABASE [CRUD] SET TARGET_RECOVERY_TIME = 60 SECONDS
GO
ALTER DATABASE [CRUD] SET DELAYED_DURABILITY = DISABLED
GO
ALTER DATABASE [CRUD] SET ACCELERATED_DATABASE_RECOVERY = OFF
GO
EXEC sys.sp_db_vardecimal_storage_format N'CRUD', N'ON'
GO
ALTER DATABASE [CRUD] SET QUERY_STORE = OFF
GO
USE [CRUD]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Product](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Description] [nvarchar](max) NOT NULL,
[Code] [nchar](10) NOT NULL,
[Count] [int] NULL,
[ProduceDate] [datetime] NOT NULL,
[Active] [bit] NOT NULL,
CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
USE [master]
GO
ALTER DATABASE [CRUD] SET READ_WRITE
GO

Kontroler
Każda operacja będzie posiadać swój własny kontroler. W skrócie kontroler przyjmuje dane wejściowe od użytkownika i reaguje na jego poczynania, zarządzając aktualizacje modelu oraz odświeżenie widoków. Mam w planach stworzyć kilka wpisów na temat architektury oprogramowania i znajdzie się tam miejsce na wzorzec architektoniczny MVC (Model-View-Controller). Szybko pokażę Ci grafikę, która pomoże zrozumieć „przepływ” w CRUDowej aplikacji.
- Użytkownik używa kontrolera (CZERWONA STRZAŁKA);
- Kontroler korzysta z modelu/modyfikuje go (ZIELONA STRZAŁKA);
- Model aktualizuje widok (POMARAŃCZOWA STRZAŁKA);
- Widok prezentuje się użytkownikowi (NIEBIESKA STRZAŁKA);
W moim przypadku kontroler będzie posiadał następujące metody:
-
Index (get) – wczytanie widoku głównego, w której znajduje się tabela wraz z wszystkimi produktami;
- Details (get) – wczyta widok szczegółów, w którym możemy sprawdzić detale konkretnego jednego produktu;
- Create (get) – wczyta widok pozwalający utworzyć nowy produkt;
- Create (post) – przetworzy widok tworzenia nowego produktu podczas kliknięcia przycisku Create;
- Edit (get) – wczyta widok pozwalający edytować istniejący produkt;
- Edit (post) – przetworzy widok edytowania istniejącego produktu podczas kliknięcia przycisku Edit;
- Delete (get) – wczyta widok pozwalający usunąć istniejący produkt;
- Delete (post) – przetworzy widok usuwania istniejącego produktu podczas kliknięcia przycisku Delete;
Nazwy kontrolerów widoczne są również w adresie URL w przeglądarce. Dla przykładu, jeśli chcę edytować produkt o ID 5, to adres będzie wyglądał następująco: https://mojadomena.pl/Edit?id=5. Szczegóły produktu o ID 5 sprawdzę pod adresem https://mojadomena.pl/Details?id=5. I tak dalej. Kod klasy ProductController dostępny jest poniżej.
using Microsoft.AspNetCore.Mvc;
namespace CRUD.Controllers
{
public class ProductController : Controller
{
// GET: ProductController
public ActionResult Index()
{
return View();
}
// GET: ProductController/Details/5
public ActionResult Details(int id)
{
return View();
}
// GET: ProductController/Create
public ActionResult Create()
{
return View();
}
// POST: ProductController/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
// GET: ProductController/Edit/5
public ActionResult Edit(int id)
{
return View();
}
// POST: ProductController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
// GET: ProductController/Delete/5
public ActionResult Delete(int id)
{
return View();
}
// POST: ProductController/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id, IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
}
}
Model
Na potrzeby tej prostej aplikacji będę tworzył jedną klasę będącą modelem produktu. Zawierać ona musi dokładnie te same pola, które znajdują się w tabeli Product w bazie danych. Ważne jest, by w modelu dokładnie określić format daty ProduceDate. Pozostałe pola są zrozumiałe. Modelu tego będziemy używać do aktualizacji oraz dodawania nowych produktów i przede wszystkim do prezentowania danych.
using System.ComponentModel.DataAnnotations;
namespace CRUD.Model
{
public class ProductModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Code { get; set; }
public int Count { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ProduceDate { get; set; }
public bool Active { get; set; }
}
}
Widoki
Widoki to największa część kodu, ponieważ każda metoda GET z kontrolera ma swój odpowiednik w widoku. W dotNET do opisywania widoków używa się języka HTML ze specjalnie przygotowanym silnikiem renderującym o nazwie RAZOR. Pozwala on łączyć kod języka C# z językiem HTML, więc jeśli uczysz się innego języka, takiego jak Python, JS, PHP czy Java, to Twój kod będzie wyglądał inaczej. Najważniejsze jest, by zrozumieć koncepcję tego, czym jest CRUD i jak ważna jest jego znajomość na początku kariery programisty. Poniżej zobacz kod wraz z widokiem z przeglądarki.
Strona główna
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/CRUD.styles.css" asp-append-version="true" />
</head>
<body>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Edycja
@page
@model CRUD.Pages.EditModel
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>ProductModel</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="ProductModel.ID" />
<div class="form-group">
<label asp-for="ProductModel.Name" class="control-label"></label>
<input asp-for="ProductModel.Name" class="form-control" />
<span asp-validation-for="ProductModel.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Description" class="control-label"></label>
<input asp-for="ProductModel.Description" class="form-control" />
<span asp-validation-for="ProductModel.Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Code" class="control-label"></label>
<input asp-for="ProductModel.Code" class="form-control" />
<span asp-validation-for="ProductModel.Code" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Count" class="control-label"></label>
<input asp-for="ProductModel.Count" class="form-control" />
<span asp-validation-for="ProductModel.Count" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.ProduceDate" class="control-label"></label>
<input asp-for="ProductModel.ProduceDate" class="form-control" />
<span asp-validation-for="ProductModel.ProduceDate" class="text-danger"></span>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="ProductModel.Active" /> @Html.DisplayNameFor(model => model.ProductModel.Active)
</label>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Usuwanie
@page
@model CRUD.Pages.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>ProductModel</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Description)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Description)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Code)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Code)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Count)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Count)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.ProduceDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.ProduceDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Active)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Active)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="ProductModel.ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>

Dodawanie
@page
@model CRUD.Pages.CreateModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>ProductModel</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProductModel.Name" class="control-label"></label>
<input asp-for="ProductModel.Name" class="form-control" />
<span asp-validation-for="ProductModel.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Description" class="control-label"></label>
<input asp-for="ProductModel.Description" class="form-control" />
<span asp-validation-for="ProductModel.Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Code" class="control-label"></label>
<input asp-for="ProductModel.Code" class="form-control" />
<span asp-validation-for="ProductModel.Code" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.Count" class="control-label"></label>
<input asp-for="ProductModel.Count" class="form-control" />
<span asp-validation-for="ProductModel.Count" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ProductModel.ProduceDate" class="control-label"></label>
<input asp-for="ProductModel.ProduceDate" class="form-control" />
<span asp-validation-for="ProductModel.ProduceDate" class="text-danger"></span>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input class="form-check-input" asp-for="ProductModel.Active" /> @Html.DisplayNameFor(model => model.ProductModel.Active)
</label>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Szczegóły
@page
@model CRUD.Pages.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>ProductModel</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Description)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Description)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Code)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Code)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Count)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Count)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.ProduceDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.ProduceDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.ProductModel.Active)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.ProductModel.Active)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.ProductModel.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>

Czym jest CRUD — Podsumowanie
CRUD może wydawać się trudny na początku, ale jeśli szukasz pierwszej pracy, to koniecznie dodaj swoje rozwiązanie do portfolio! Możesz dzięki takiemu projektowi zrobić swoją pierwszą aplikację, na przykład często polecana na początku aplikacja do tworzenia listy zadań lub listy zakupów. CRUD w tym przypadku sprawdzi się idealnie jako szablon na start.
Oczywiście ten projekt znajdziesz na moim koncie GitHub, więc jeśli pracujesz w języku C#, to koniecznie go pobieraj i zmodyfikuje pod własne potrzeby. Daj znać w komentarzu, czy wpis Ci pomógł, bo teraz już mam nadzieję, że wiesz, czym jest CRUD. Z tego miejsca zapraszam Cię do kolejnego wpisu, jakim jest wpis na temat liczb zmiennoprzecinkowych.
Newsletter
Nie przegap i dołącz już dziś do 838 osób będących w tym Newsletter! Otrzymuj co niedzielę o godzinie 20 listę kilku ciekawych tematów, które miałem okazję obserwować w mijającym tygodniu.
Tematy będą głównie techniczne, ale czasami pojawi się coś, co może wprowadzi Cię w stan zadumy i zmusi do dyskusji w szerszym gronie. Zero spamu!