Självstudie: Implementera CRUD-funktioner med Entity Framework i ASP.NET MVC

I den föregående självstudien skapade du ett MVC-program som lagrar och visar data med hjälp av Entity Framework (EF) 6 och SQL Server LocalDB. I den här självstudien granskar och anpassar du den CRUD-kod (create, read, update, delete) som MVC-ställningskod automatiskt skapar åt dig i styrenheter och vyer.

Note

Det är vanligt att implementera lagringsplatsens mönster för att skapa ett abstraktionslager mellan kontrollanten och dataåtkomstskiktet. För att hålla de här självstudierna enkla och fokuserade på att lära ut hur man använder SJÄLVA EF 6 använder de inte repositories. Information om hur du implementerar repositorier finns i ASP.NET Data Access Content Map.

Här är exempel på de webbsidor som du skapar:

En skärmbild av sidan med elevinformation.

En skärmbild av sidan Skapa elev.

En skärmbild av sidan för att ta bort en student.

I den här handledningen kommer du att:

  • Skapa en detaljsida
  • Uppdatera sidan Skapa
  • Uppdatera metoden HttpPost-redigering
  • Uppdatera sidan Ta bort
  • Stäng databasanslutningar
  • Hantera transaktioner

Prerequisites

Skapa en informationssida

Den kod som skapats för sidan Studenter Index utelämnade Enrollments egenskapen eftersom egenskapen innehåller en samling. På sidan Details visar du innehållet i samlingen i en HTML-tabell.

I Controllers\StudentController.cs använder åtgärdsmetoden för Details-vyn metoden Find för att hämta en enda Student-entitet.

public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

Nyckelvärdet skickas till metoden som parametern id och kommer från routningsdata i hyperlänken Detaljer på sidan Index.

Tips Routningsdata

Routningsdata är data som modellbindaren hittade i ett URL-segment som anges i routningstabellen. Exempelvis anger standardvägen: controller, action och id segment:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

I följande URL mappar Instructor standardvägen som controller, Index som action och 1 som id; dessa är routningsdatavärden.

http://localhost:1230/Instructor/Index/1?courseID=2021

?courseID=2021 är ett värde i frågesträngen. Modellbindningen fungerar också om du skickar id som ett frågesträngsvärde:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

URL:erna skapas av ActionLink -instruktioner i Razor-vyn. I följande kod matchar parametern id standardvägen, så id läggs till i routningsdata.

@Html.ActionLink("Select", "Index", new { id = item.PersonID  })

I följande kod courseID matchar inte en parameter i standardvägen, så den läggs till som en frågesträng.

@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })

Så här skapar du sidan Information

  1. Öppna Views\Student\Details.cshtml.

    Varje fält visas med hjälp av en DisplayFor hjälp, som du ser i följande exempel:

    <dt>
        @Html.DisplayNameFor(model => model.LastName)
    </dt>
    <dd>
        @Html.DisplayFor(model => model.LastName)
    </dd>
    
  2. Efter fältet EnrollmentDate och omedelbart före den avslutande </dl> taggen lägger du till den markerade koden för att visa en lista över registreringar, som du ser i följande exempel:

    <dt>
                @Html.DisplayNameFor(model => model.EnrollmentDate)
            </dt>
    
            <dd>
                @Html.DisplayFor(model => model.EnrollmentDate)
            </dd>
            <dt>
                @Html.DisplayNameFor(model => model.Enrollments)
            </dt>
            <dd>
                <table class="table">
                    <tr>
                        <th>Course Title</th>
                        <th>Grade</th>
                    </tr>
                    @foreach (var item in Model.Enrollments)
                    {
                        <tr>
                            <td>
                                @Html.DisplayFor(modelItem => item.Course.Title)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => item.Grade)
                            </td>
                        </tr>
                    }
                </table>
            </dd>
        </dl>
    </div>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>
    

    Om kod indraget är fel när du har klistrat in koden trycker du på Ctrl+K, Ctrl+D för att formatera den.

    Den här koden loopar igenom entiteterna i navigeringsegenskapen Enrollments . För varje Enrollment entitet i egenskapen visas kursrubriken och betyget. Kurstiteln hämtas från entiteten Course som lagras i Course entitetens navigeringsegenskap Enrollments . Alla dessa data hämtas automatiskt från databasen när de behövs. Med andra ord använder du lazy loading här. Du angav inte eager loading för navigeringsegenskapen Courses, så registreringarna hämtades inte i samma fråga som hämtade eleverna. Första gången du försöker komma åt navigeringsegenskapen Enrollments skickas i stället en ny fråga till databasen för att hämta data. Du kan läsa mer om lazy loading och eager loading i självstudien Reading Related Data senare i den här serien.

  3. Öppna sidan Detaljer genom att starta programmet (Ctrl+F5), välja fliken Studenter och sedan klicka på länken Detaljer för Alexander Carson. (Om du trycker på Ctrl+F5 medan filen Details.cshtml är öppen får du ett HTTP 400-fel. Det beror på att Visual Studio försöker köra sidan Details, men den nåddes inte från en länk som anger vilken elev som ska visas. Om det händer tar du bort "Student/Details" från URL:en och försöker igen, eller stänger webbläsaren, högerklickar på projektet och klickar på Visa>Visa i webbläsare.)

    Du ser listan över kurser och betyg för den valda eleven.

  4. Stäng webbläsaren.

Uppdatera sidan Skapa

  1. I filen Controllers\StudentController.cs ersätter du åtgärdsmetoden HttpPostAttributeCreate med följande kod. Den här koden lägger till ett try-catch block och tar bort ID från BindAttribute attributet för den scaffolded -metoden:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student)
    {
        try
        {
            if (ModelState.IsValid)
            {
                db.Students.Add(student);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
        }
        catch (DataException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
        }
        return View(student);
    }
    

    Den här koden lägger till entiteten Student som skapats av ASP.NET MVC-modellbindaren till entitetsuppsättningen Students och sparar sedan ändringarna i databasen. Modellbindare refererar till ASP.NET MVC-funktionalitet som gör det enklare för dig att arbeta med data som skickas av ett formulär. En modellbindare konverterar inskickade formulärvärden till CLR-typer och skickar dem som parametrar till åtgärdsmetoden. I det här fallet instansierar modellbindaren en entitet Student åt dig med hjälp av egenskapsvärden från Form samlingen.

    Du har tagit bort ID från attributet Bind eftersom ID är det primära nyckelvärde som SQL Server anger automatiskt när raden infogas. Indata från användaren ställer inte in värdet ID.

    Säkerhetsvarning – Attributet ValidateAntiForgeryToken hjälper till att förhindra begärandeförfalskning mellan webbplatser-attacker. Det kräver en motsvarande Html.AntiForgeryToken() sats i vyn, som du kommer att se senare.

    Bind-attributet är ett sätt att skydda mot överpostning vid skapande. Anta till exempel att entiteten Student innehåller en Secret egenskap som du inte vill att den här webbsidan ska ange.

    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
        public string Secret { get; set; }
    
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
    

    Även om du inte har något Secret-fält på webbsidan kan en hackare använda ett verktyg som Fiddler eller skriva JavaScript för att skicka in ett Secret-formulärvärde. Utan attributet BindAttribute som begränsar de fält som modellbindaren använder när den skapar en Student-instans, skulle modellbindaren fånga upp formulärvärdet Secret och använda det för att skapa entitetsinstansen Student. Då skulle det värde som hackaren angav för Secret-formulärfältet uppdateras i din databas. Följande bild visar verktyget Fiddler som lägger till fältet Secret (med värdet "OverPost") i de postade formulärvärdena.

    Skärmbilden visar fliken Composer. I det övre högra hörnet är Execute inringat i rött. I det nedre högra hörnet är Secret = Over Post inringat i rött.

    Värdet "OverPost" skulle då framgångsrikt läggas till i egenskapen Secret för den infogade raden, även om du aldrig avsåg att webbsidan skulle kunna ställa in den egenskapen.

    Det är bäst att använda parametern Include med attributet Bind för att explicit ange fält. Du kan också använda parametern Exclude för att blockera fält som du vill exkludera. Anledningen till att Include är säkrare är att när du lägger till en ny egenskap i entiteten skyddas inte det nya fältet automatiskt av en Exclude lista.

    Du kan förhindra överpublicering i redigeringsscenarier genom att först läsa entiteten från databasen och sedan anropa TryUpdateModel, och skicka in en explicit lista över tillåtna egenskaper. Det är den metod som används i de här självstudierna.

    Ett alternativt sätt att förhindra overposting som många utvecklare föredrar är att använda vymodeller i stället för entitetsklasser med modellbindning. Inkludera endast de egenskaper som du vill uppdatera i vymodellen. När MVC-modellbindningen har slutförts kopierar du egenskaperna för visningsmodellen till entitetsinstansen, eventuellt med hjälp av ett verktyg som AutoMapper. Använd db. Post på entitetsinstansen för att ange dess tillstånd till Oförändrat och ange sedan Property("PropertyName"). IsModified till true för varje entitetsegenskap som ingår i vymodellen. Den här metoden fungerar både i redigerings- och skapandescenarier.

    Förutom Bind attributet är try-catch blocket den enda ändring som du har gjort i den scaffolded koden. Om ett undantag som härleds från DataException fångas när ändringarna sparas visas ett allmänt felmeddelande. DataException undantag orsakas ibland av något utanför programmet i stället för ett programmeringsfel, så användaren uppmanas att försöka igen. Även om det inte implementeras i det här exemplet skulle en produktionsfärdig applikation logga undantaget. Mer information finns i avsnittet Log for insight i Övervakning och telemetri (Skapa Real-World Cloud Apps med Azure).

    Koden i Views\Student\Create.cshtml liknar det du såg i Details.cshtml, förutom att EditorFor och ValidationMessageFor helpermetoder används för varje fält i stället för DisplayFor. Här är den relevanta koden:

    <div class="form-group">
        @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>
    </div>
    

    Create.cshtml innehåller även @Html.AntiForgeryToken(), som fungerar med attributet ValidateAntiForgeryToken i controllern för att förhindra förfalskningsattacker mellan webbplatser.

    Inga ändringar krävs i Create.cshtml.

  2. Öppna sidan genom att starta programmet, välja fliken Studenter och klicka sedan på Skapa ny.

  3. Ange namn och ett ogiltigt datum och klicka på Skapa för att se felmeddelandet.

    Det här är validering på serversidan som du får som standard. I en senare självstudie får du se hur du lägger till attribut som genererar kod för validering på klientsidan. Följande markerade kod visar modellverifieringskontrollen i metoden Create.

    if (ModelState.IsValid)
    {
        db.Students.Add(student);
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    
  4. Ändra datumet till ett giltigt värde och klicka på Skapa för att se den nya eleven visas på sidan Index .

  5. Stäng webbläsaren.

Uppdatera Edit-metoden för HttpPost

  1. HttpPostAttribute Edit Ersätt action-metoden med följande kod:

    [HttpPost, ActionName("Edit")]
    [ValidateAntiForgeryToken]
    public ActionResult EditPost(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        var studentToUpdate = db.Students.Find(id);
        if (TryUpdateModel(studentToUpdate, "",
           new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
        {
            try
            {
                db.SaveChanges();
    
                return RedirectToAction("Index");
            }
            catch (DataException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
            }
        }
        return View(studentToUpdate);
    }
    

    Note

    I Controllers\StudentController.cs använder HttpGet Edit metoden (den utan Find attributet) HttpPost metoden för att hämta den valda Student entiteten, som du såg i Details metoden. Du behöver inte ändra den här metoden.

    Dessa ändringar implementerar bästa praxis för säkerhet för att förhindra overposting. Scaffolder genererade ett Bind attribut och lade till entiteten som skapades av modellbindaren i entitetsuppsättningen med flaggan Ändrad. Koden rekommenderas inte längre eftersom attributet Bind rensar bort redan befintliga data i fält som inte anges i parametern Include. I framtiden uppdateras MVC-styrenhetens scaffolder så att den inte genererar Bind attribut för Redigeringsmetoder.

    Den nya koden läser den befintliga entiteten och anropar TryUpdateModel för att uppdatera fält från användarindata i inskickade formulärdata. Entity Frameworks automatiska ändringsspårning sätter flaggan EntityState.Modified på entiteten. När metoden SaveChanges anropas gör flaggan Modified att Entity Framework skapar SQL-instruktioner för att uppdatera databasraden. Samtidighetskonflikter ignoreras, och alla kolumner i databasraden uppdateras, inklusive de som användaren inte ändrade. (En senare självstudie visar hur du hanterar samtidighetskonflikter, och om du bara vill att enskilda fält ska uppdateras i databasen kan du ange entiteten till EntityState.Unchanged och ange enskilda fält till EntityState.Modified.)

    För att förhindra overposting listas de fält som du vill ska vara uppdateringsbara via redigeringssidan i parametrarna TryUpdateModel. För närvarande finns det inga extra fält som du skyddar, men att ange de fält som du vill att modellbindaren ska binda till säkerställer att om du lägger till fält i datamodellen i framtiden skyddas de automatiskt tills du uttryckligen lägger till dem här.

    Till följd av dessa ändringar är metodsignaturen för HttpPost-metoden Edit densamma som för HttpGet-metoden Edit. Därför har du döpt om metoden till EditPost.

    Tip

    Entitetstillstånd och metoderna Attach och SaveChanges

    Databaskontexten håller reda på om entiteter i minnet är synkroniserade med motsvarande rader i databasen, och den här informationen avgör vad som händer när du anropar SaveChanges metoden. När du till exempel skickar en ny entitet till metoden Lägg till anges den entitetens tillstånd till Added. När du sedan anropar metoden SaveChanges utfärdar databaskontexten ett SQL-kommando INSERT .

    En entitet kan vara i något av följande tillstånd:

    • Added. Entiteten finns ännu inte i databasen. Metoden SaveChanges måste utfärda en INSERT -instruktion.
    • Unchanged. Inget behöver göras med den här entiteten med metoden SaveChanges . När du läser en entitet från databasen börjar entiteten med den här statusen.
    • Modified. Vissa eller alla entitets egenskapsvärden har ändrats. Metoden SaveChanges måste utfärda en UPDATE -instruktion.
    • Deleted. Entiteten har markerats för borttagning. Metoden SaveChanges måste utfärda en DELETE -instruktion.
    • Detached. Entiteten spåras inte av databaskontexten.

    I ett skrivbordsprogram anges vanligtvis tillståndsändringar automatiskt. I en skrivbordstyp av program läser du en entitet och gör ändringar i vissa av dess egenskapsvärden. Detta gör att dess entitetstillstånd ändras automatiskt till Modified. När du sedan anropar SaveChangesgenererar Entity Framework en SQL-instruktion UPDATE som endast uppdaterar de faktiska egenskaper som du har ändrat.

    Webbapparnas frånkopplade karaktär tillåter inte den här kontinuerliga sekvensen. DbContext som läser en entitet tas bort när en sida återges. När åtgärdsmetoden anropas görs en ny begäran och du har en ny instans av HttpPost, så du måste manuellt ange entitetstillståndet till Edit Sedan när du anropar uppdaterar Entity Framework alla kolumner i databasraden, eftersom kontexten inte har något sätt att veta vilka egenskaper du har ändrat.Modified.SaveChanges

    Om du vill att SQL-instruktionen Update endast ska uppdatera de fält som användaren faktiskt har ändrat kan du spara de ursprungliga värdena på något sätt (till exempel dolda fält) så att de är tillgängliga när HttpPostEdit metoden anropas. Sedan kan du skapa en Student-entitet med hjälp av de ursprungliga värdena, anropa metoden Attach med den ursprungliga versionen av entiteten, uppdatera entitetens värden till de nya värdena och sedan anropa SaveChanges.. För mer information, se Entitetstillstånd och SaveChanges och Lokal data.

    HTML- och Razor-koden i Views\Student\Edit.cshtml liknar det du såg i Create.cshtml och inga ändringar krävs.

  2. Kör sidan genom att starta programmet, välja fliken Studenter och sedan klicka på en Redigera hyperlänk.

  3. Ändra vissa data och klicka på Spara. Du ser ändrade data på sidan Index.

  4. Stäng webbläsaren.

Uppdatera sidan Ta bort

I Controllers\StudentController.cs använder HttpGetAttribute mallkoden för DeleteFind metoden för att hämta den valda Student entiteten, som du såg i Details och Edit metoderna. Men för att implementera ett anpassat felmeddelande när anropet till SaveChanges misslyckas lägger du till viss funktionalitet i den här metoden och dess motsvarande vy.

Som du såg för att uppdatera och skapa åtgärder kräver borttagningsåtgärder två åtgärdsmetoder. Metoden som anropas som svar på en GET-begäran visar en vy som ger användaren en chans att godkänna eller avbryta borttagningsåtgärden. Om användaren godkänner det skapas en POST-begäran. När det händer HttpPostDelete anropas metoden och den metoden utför faktiskt borttagningsåtgärden.

Du lägger till ett try-catch block i HttpPostAttributeDelete metoden för att hantera eventuella fel som kan uppstå när databasen uppdateras. Om ett fel inträffar HttpPostAttributeDelete anropar HttpGetAttributeDelete metoden metoden och skickar den en parameter som anger att ett fel har inträffat. Metoden HttpGetAttributeDelete visar sedan bekräftelsesidan igen tillsammans med felmeddelandet, vilket ger användaren möjlighet att avbryta eller försöka igen.

  1. HttpGetAttribute Delete Ersätt åtgärdsmetoden med följande kod, som hanterar felrapportering:

    public ActionResult Delete(int? id, bool? saveChangesError=false)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        if (saveChangesError.GetValueOrDefault())
        {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
        }
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
    

    Den här koden tar emot en valfri parameter som anger om metoden anropades efter ett misslyckat försök att spara ändringar. Den här parametern är false när HttpGetDelete metoden anropas utan ett tidigare fel. När den anropas av HttpPostDelete metoden som svar på ett databasuppdateringsfel är true parametern och ett felmeddelande skickas till vyn.

  2. HttpPostAttribute Delete Ersätt åtgärdsmetoden (med namnet DeleteConfirmed) med följande kod, som utför den faktiska borttagningsåtgärden och fångar upp eventuella databasuppdateringsfel.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
        }
        return RedirectToAction("Index");
    }
    

    Den här koden hämtar den valda entiteten och anropar sedan metoden Ta bort för att ange entitetens status till Deleted. När SaveChanges anropas genereras ett SQL-kommando DELETE . Du har också ändrat åtgärdsmetodnamnet från DeleteConfirmed till Delete. Genererad kod gav HttpPost-metoden namnet DeleteDeleteConfirmed för att ge metoden HttpPost en unik signatur. (CLR kräver att överlagrade metoder har olika metodparametrar.) Nu när signaturerna är unika kan du hålla dig till MVC-konventionen och använda samma namn för HttpPost metoderna och HttpGet ta bort.

    Om det är en prioritet att förbättra prestandan i ett program med hög volym, kan du undvika en onödig SQL-fråga för att hämta raden genom att ersätta de kodrader som anropar metoderna Find och Remove med följande kod:

    Student studentToDelete = new Student() { ID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;
    

    Den här koden instansierar en entitet Student med endast primärnyckelvärdet och anger sedan entitetstillståndet till Deleted. Det är allt som Entity Framework behöver för att ta bort entiteten.

    Som nämnts HttpGetDelete tar metoden inte bort data. Att utföra en borttagningsåtgärd som svar på en GET-begäran (eller för den delen att utföra en redigeringsåtgärd, skapa åtgärd eller någon annan åtgärd som ändrar data) skapar en säkerhetsrisk.

  3. I Views\Student\Delete.cshtml lägger du till ett felmeddelande mellan h2 rubriken och h3 rubriken, enligt följande exempel:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>
    
  4. Kör sidan genom att starta programmet, välja fliken Studenter och sedan klicka på en Ta bort hyperlänk.

  5. Välj Ta bort på sidan med texten Vill du ta bort det här?.

    Sidan Index visas utan den borttagna eleven. (Du kommer att se ett exempel på felhanteringskoden i praktiken i självstudiekursen om samtidighet.)

Stäng databasanslutningar

För att stänga databasanslutningar och frigöra resurserna de använder så snart som möjligt avslutar du kontextinstansen när du är klar med den. Det är därför som den genererade koden innehåller en Dispose-metod i slutet av StudentController klassen i StudentController.cs, som du ser i följande exempel:

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

Controller Basklassen implementerar redan gränssnittet, så den här IDisposable koden lägger helt enkelt till en överskrivning av Dispose(bool) metoden för att uttryckligen frigöra kontextinstansen.

Hantera transaktioner

Som standard implementerar Entity Framework implicit transaktioner. I scenarier där du gör ändringar i flera rader eller tabeller och sedan anropar SaveChangesser Entity Framework automatiskt till att alla dina ändringar lyckas eller att alla misslyckas. Om vissa ändringar görs först och sedan inträffar ett fel, återställs dessa ändringar automatiskt. För scenarier där du behöver mer kontroll, till exempel om du vill inkludera åtgärder som utförs utanför Entity Framework i en transaktion, se Arbeta med transaktioner.

Ytterligare resurser

Nu har du en fullständig uppsättning sidor som utför enkla CRUD-åtgärder för Student entiteter. Du använde MVC-hjälpen för att generera användargränssnittselement för datafält. Mer information om MVC-hjälpen finns i Rendering a Form Using HTML Helpers (artikeln är för MVC 3 men är fortfarande relevant för MVC 5).

Länkar till andra EF 6-resurser finns i ASP.NET Data Access – Rekommenderade resurser.

Nästa steg

I den här handledningen kommer du att:

  • En detaljsida har skapats
  • Sidan Skapa har uppdaterats
  • HttpPost Edit-metoden har uppdaterats
  • Sidan Ta bort har uppdaterats
  • Stängda databasanslutningar
  • Hanterade transaktioner

Gå vidare till nästa artikel för att lära dig hur du lägger till sortering, filtrering och sidindelning i projektet.