Kurz: Čtení souvisejících dat pomocí EF v aplikaci ASP.NET MVC
V předchozím kurzu jste dokončili datový model School. V tomto kurzu si přečtete a zobrazíte související data – to znamená data, která Entity Framework načte do navigačních vlastností.
Následující ilustrace znázorňují stránky, se kterými budete pracovat.
Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 5 pomocí entity Framework 6 Code First a sady Visual Studio. Informace o sérii kurzů najdete v prvním kurzu série.
V tomto kurzu se naučíte:
- Naučte se načítat související data.
- Vytvoření stránky Kurzy
- Vytvoření stránky instruktorů
Požadavky
Naučte se načítat související data.
Entity Framework může načíst související data do navigačních vlastností entity několika způsoby:
Opožděné načítání. Při prvním čtení entity se související data nenačtou. Při prvním pokusu o přístup k navigační vlastnosti se však automaticky načtou data potřebná pro tuto navigační vlastnost. Výsledkem je více dotazů odeslaných do databáze – jeden pro samotnou entitu a jeden při každém načtení souvisejících dat pro danou entitu. Třída
DbContext
ve výchozím nastavení umožňuje opožděné načítání.Dychtivá načítání. Při čtení entity se spolu s ní načtou související data. Výsledkem je obvykle jeden dotaz spojení, který načte všechna potřebná data. Pomocí metody určíte dychtivé načítání
Include
.Explicitní načítání. To se podobá opožděné načítání, s tím rozdílem, že explicitně načítáte související data v kódu; při přístupu k navigační vlastnosti k ní nedojde automaticky. Související data načtete ručně získáním položky správce stavu objektu pro entitu a voláním Collection.Load metody pro kolekce nebo Metodu Reference.Load pro vlastnosti, které obsahují jednu entitu. (Pokud byste v následujícím příkladu chtěli načíst vlastnost Navigace správcem, nahradíte
Collection(x => x.Courses)
jiReference(x => x.Administrator)
.) Explicitní načítání byste obvykle používali jenom v případech, kdy jste vypnuli opožděné načítání.
Protože nenačítají hodnoty vlastností okamžitě, opožděné načítání a explicitní načítání se také označuje jako odložené načítání.
Důležité informace o výkonu
Pokud víte, že potřebujete související data pro každou načtenou entitu, načítání dychtivosti často nabízí nejlepší výkon, protože jeden dotaz odeslaný do databáze je obvykle efektivnější než samostatné dotazy pro každou načtenou entitu. Předpokládejme například, že v předchozích příkladech má každé oddělení deset souvisejících kurzů. Příklad dychtivého načítání by výsledkem byl pouze jeden dotaz (spojení) a jedna odezva do databáze. Opožděné načítání a explicitní načítání příkladů by vedlo k jedenácti dotazům i k jedenácti odezvám do databáze. Dodatečné odezvy databáze jsou zvláště škodlivé pro výkon, pokud je latence vysoká.
Na druhou stranu je v některých scénářích opožděné načítání efektivnější. Dychtivá načítání může způsobit vygenerování velmi složitého spojení, které SQL Server nedokáže efektivně zpracovat. Nebo pokud potřebujete získat přístup k navigačním vlastnostem entity pouze pro podmnožinu sady entit, které zpracováváte, může opožděné načítání lépe fungovat, protože by dychtivé načítání načítalo více dat, než potřebujete. Pokud je výkon kritický, je nejlepší otestovat výkon oběma způsoby, aby byla nejlepší volbou.
Opožděné načítání může maskovat kód, který způsobuje problémy s výkonem. Například kód, který nezadá dychtivé nebo explicitní načítání, ale zpracovává velký objem entit a používá v každé iteraci několik navigačních vlastností, může být velmi neefektivní (kvůli mnoha odezvám do databáze). Aplikace, která funguje dobře při vývoji pomocí místního SQL Serveru, může mít problémy s výkonem při přesunu do služby Azure SQL Database z důvodu zvýšené latence a opožděného načítání. Profilace databázových dotazů s realistickým testovacím zatížením vám pomůže určit, jestli je vhodné opožděné načítání. Další informace najdete v tématu Demystifying Entity Framework Strategies: Načítání souvisejících dat a použití entity Framework ke snížení latence sítě do SQL Azure.
Zakázat opožděné načítání před serializací
Pokud necháte opožděné načítání povolené během serializace, můžete nakonec dotazovat výrazně více dat, než jste chtěli. Serializace obecně funguje tak, že přistupuje ke každé vlastnosti v instanci typu. Přístup k vlastnostem aktivuje opožděné načítání a tyto opožděné načtené entity jsou serializovány. Proces serializace pak přistupuje ke každé vlastnosti opožděných načtených entit, což může způsobit ještě opožděnější načítání a serializaci. Chcete-li zabránit této řetězové reakci na běh, vypněte opožděné načítání před serializací entity.
Serializace může být také komplikovaná třídami proxy, které Entity Framework používá, jak je vysvětleno v kurzu Advanced Scenarios.
Jedním ze způsobů, jak se vyhnout problémům serializace, je serializovat objekty přenosu dat (DTO) místo objektů entit, jak je znázorněno v kurzu Použití webového rozhraní API s Entity Framework .
Pokud nepoužíváte DTO, můžete zakázat opožděné načítání a vyhnout se problémům s proxy serverem zakázáním vytváření proxy serveru.
Tady je několik dalších způsobů, jak zakázat opožděné načítání:
Pro konkrétní navigační vlastnosti vynecháte
virtual
klíčové slovo při deklaraci vlastnosti.U všech navigačních vlastností nastavte
LazyLoadingEnabled
nafalse
hodnotu , vložte do konstruktoru třídy kontextu následující kód:this.Configuration.LazyLoadingEnabled = false;
Vytvoření stránky Kurzy
Entita Course
obsahuje navigační vlastnost, která obsahuje entitu Department
oddělení, ke kterému je kurz přiřazen. Pokud chcete zobrazit název přiřazeného oddělení v seznamu kurzů, musíte vlastnost získat Name
z Department
entity, která je v Course.Department
navigační vlastnosti.
Vytvořte kontroler s názvem CourseController
(nikoli CoursesController) pro Course
typ entity pomocí stejných možností pro kontroler MVC 5 se zobrazeními pomocí správce Entity Framework , který jste provedli dříve pro Student
kontroler:
Nastavení | Hodnota |
---|---|
Třída modelu | Vyberte Kurz (ContosoUniversity.Models). |
Třída kontextu dat | Vyberte SchoolContext (ContosoUniversity.DAL). |
Název kontroleru | Zadejte CourseController. Znovu, ne CoursesController s s. Když jste vybrali kurz (ContosoUniversity.Models), automaticky se vyplní hodnota názvu kontroleru. Musíte změnit hodnotu. |
Ponechte ostatní výchozí hodnoty a přidejte kontroler.
Otevřete Controllers\CourseController.cs a podívejte se na metodu Index
:
public ActionResult Index()
{
var courses = db.Courses.Include(c => c.Department);
return View(courses.ToList());
}
Automatické generování uživatelského rozhraní pomocí metody určilo dychtivé načítání pro Department
navigační vlastnost Include
.
Otevřete Views\Course\Index.cshtml a nahraďte kód šablony následujícím kódem. Změny jsou zvýrazněné:
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewBag.Title = "Courses";
}
<h2>Courses</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
Department
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
@Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
</td>
</tr>
}
</table>
Vygenerovaný kód jste provedli následující změny:
- Změnili jsme nadpis z indexu na Kurzy.
- Přidali jsme sloupec Číslo , který zobrazuje
CourseID
hodnotu vlastnosti. Ve výchozím nastavení se primární klíče nevygenerují, protože obvykle nejsou pro koncové uživatele nesmyslné. V tomto případě je ale primární klíč smysluplný a chcete ho zobrazit. - Přesunuli jsme sloupec Oddělení na pravou stranu a změnili jeho záhlaví. Správce scaffolder se správně rozhodl zobrazit
Name
vlastnost zDepartment
entity, ale tady na stránce Kurz by záhlaví sloupce mělo být Oddělení místo názvu.
Všimněte si, že pro sloupec Oddělení zobrazuje Name
vygenerovaný kód vlastnost Department
entity, která je načtena do Department
navigační vlastnosti:
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
Spusťte stránku (vyberte kartu Kurzy na domovské stránce Contoso University) a zobrazte seznam s názvy oddělení.
Vytvoření stránky instruktorů
V této části vytvoříte kontroler a zobrazíte entitu Instructor
, abyste zobrazili stránku instruktorů. Tato stránka čte a zobrazuje související data následujícími způsoby:
- Seznam instruktorů zobrazuje související data z
OfficeAssignment
dané entity.OfficeAssignment
EntityInstructor
jsou v relaci 1:0 nebo 1. Pro entity použijete dychtivé načítáníOfficeAssignment
. Jak bylo vysvětleno dříve, načítání dychtivosti je obvykle efektivnější, když potřebujete související data pro všechny načtené řádky primární tabulky. V takovém případě chcete zobrazit zadání kanceláře pro všechny zobrazené instruktory. - Když uživatel vybere instruktora, zobrazí se související
Course
entity. EntityInstructor
aCourse
entity jsou v relaci M:N. Pro entity a souvisejícíDepartment
entity použijete dychtivé načítáníCourse
. V tomto případě může být opožděné načítání efektivnější, protože potřebujete kurzy pouze pro vybraného instruktora. Tento příklad však ukazuje, jak používat dychtivé načítání vlastností navigace v rámci entit, které jsou samy ve vlastnostech navigace. - Když uživatel vybere kurz, zobrazí se související data ze
Enrollments
sady entit. EntityCourse
aEnrollment
entity jsou v relaci 1:N. Přidáte explicitní načítání entitEnrollment
a souvisejícíchStudent
entit. (Explicitní načítání není nutné, protože je povolené opožděné načítání, ale ukazuje, jak provést explicitní načítání.)
Vytvoření modelu zobrazení pro zobrazení indexu instruktora
Na stránce Instruktori se zobrazují tři různé tabulky. Proto vytvoříte model zobrazení, který obsahuje tři vlastnosti, z nichž každá obsahuje data pro jednu z tabulek.
Ve složce ViewModels vytvořte InstructorIndexData.cs a nahraďte stávající kód následujícím kódem:
using System.Collections.Generic;
using ContosoUniversity.Models;
namespace ContosoUniversity.ViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Vytvoření kontroleru a zobrazení instruktora
Vytvoření InstructorController
kontroleru (ne InstruktorsController) s akcí EF pro čtení a zápis:
Nastavení | Hodnota |
---|---|
Třída modelu | Vyberte Instruktor (ContosoUniversity.Models). |
Třída kontextu dat | Vyberte SchoolContext (ContosoUniversity.DAL). |
Název kontroleru | Zadejte InstructorController. Znovu, ne InstruktorsController s s. Když jste vybrali kurz (ContosoUniversity.Models), automaticky se vyplní hodnota názvu kontroleru. Musíte změnit hodnotu. |
Ponechte ostatní výchozí hodnoty a přidejte kontroler.
Otevřete Controllers\InstructorController.cs a přidejte using
příkaz pro ViewModels
obor názvů:
using ContosoUniversity.ViewModels;
Vygenerovaný kód v Index
metodě určuje dychtivé načítání pouze pro OfficeAssignment
navigační vlastnost:
public ActionResult Index()
{
var instructors = db.Instructors.Include(i => i.OfficeAssignment);
return View(instructors.ToList());
}
Nahraďte metodu Index
následujícím kódem, který načte další související data a vloží je do modelu zobrazení:
public ActionResult Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(
i => i.ID == id.Value).Single().Courses;
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
Metoda přijímá volitelná směrovací data (id
) a parametr řetězce dotazu (courseID
), který poskytuje hodnoty ID vybraného instruktora a vybraného kurzu a předává všechna požadovaná data do zobrazení. Parametry poskytují hypertextové odkazy na stránce.
Kód začíná vytvořením instance modelu zobrazení a jeho vložením do seznamu instruktorů. Kód určuje dychtivé načítání pro Instructor.OfficeAssignment
vlastnost a Instructor.Courses
vlastnost navigace.
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
Druhá Include
metoda načte Courses a pro každý kurz, který je načten, nechtěně načítá pro Course.Department
navigační vlastnost.
.Include(i => i.Courses.Select(c => c.Department))
Jak už jsme zmínili dříve, načítání dychtivých požadavků se nevyžaduje, ale provádí se ke zlepšení výkonu. Vzhledem k tomu, že zobrazení vždy vyžaduje entitu OfficeAssignment
, je efektivnější ji načíst ve stejném dotazu. Course
entity jsou vyžadovány, pokud je na webové stránce vybrán instruktor, takže dychtivá načítání je lepší než opožděné načítání pouze v případě, že se stránka zobrazuje častěji s vybraným kurzem než bez.
Pokud jste vybrali ID instruktora, vybraný instruktor se načte ze seznamu instruktorů v modelu zobrazení. Vlastnost modelu Courses
zobrazení se pak načte s Course
entitami z navigační vlastnosti daného instruktora Courses
.
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}
Metoda Where
vrátí kolekci, ale v tomto případě kritéria předaná této metodě způsobí, že se vrátí pouze jedna Instructor
entita. Metoda Single
převede kolekci na jednu Instructor
entitu, která poskytuje přístup k vlastnosti dané entity Courses
.
Pokud víte, že kolekce bude obsahovat pouze jednu položku, použijete pro kolekci metodu Single . Metoda Single
vyvolá výjimku, pokud je kolekce předána je prázdná nebo pokud existuje více než jedna položka. Alternativou je SingleOrDefault, která vrátí výchozí hodnotu (null
v tomto případě), pokud je kolekce prázdná. V tomto případě by však stále došlo k výjimce (z pokusu o vyhledání Courses
vlastnosti odkazu null
) a zpráva o výjimce by méně jasně značí příčinu problému. Při volání Single
metody můžete také předat podmínku Where
namísto samostatného volání Where
metody:
.Single(i => i.ID == id.Value)
Místo:
.Where(I => i.ID == id.Value).Single()
Pokud jste vybrali kurz, vybraný kurz se načte ze seznamu kurzů v modelu zobrazení. Vlastnost modelu Enrollments
zobrazení se pak načte s Enrollment
entitami z navigační Enrollments
vlastnosti daného kurzu.
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Úprava zobrazení indexu instruktora
V views\Instructor\Index.cshtml nahraďte kód šablony následujícím kódem. Změny jsou zvýrazněné:
@model ContosoUniversity.ViewModels.InstructorIndexData
@{
ViewBag.Title = "Instructors";
}
<h2>Instructors</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th></th>
</tr>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == ViewBag.InstructorID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@Html.ActionLink("Select", "Index", new { id = item.ID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
@Html.ActionLink("Details", "Details", new { id = item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.ID })
</td>
</tr>
}
</table>
V existujícím kódu jste provedli následující změny:
Změna třídy modelu na
InstructorIndexData
.Změnili jsme název stránky z indexu na instruktory.
Přidali jsme sloupec Office , který se zobrazí
item.OfficeAssignment.Location
jenom v případěitem.OfficeAssignment
, že nemá hodnotu null. (Vzhledem k tomu, že se jedná o relaci 1:0 nebo 1, nemusí existovat souvisejícíOfficeAssignment
entita.)<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
Přidali jsme kód, který se dynamicky přidá
class="success"
dotr
prvku vybraného instruktora. Tím nastavíte barvu pozadí pro vybraný řádek pomocí třídy Bootstrap.string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow" valign="top">
Přidali jsme nový
ActionLink
popisek Select bezprostředně před ostatní odkazy v každém řádku, což způsobí, že vybrané ID instruktoraIndex
se do metody odešle.
Spusťte aplikaci a vyberte kartu Instruktori . Stránka zobrazí Location
vlastnost souvisejících OfficeAssignment
entit a prázdnou buňku tabulky, pokud neexistuje žádná související OfficeAssignment
entita.
Do souboru Views\Instructor\Index.cshtml za uzavírací table
prvek (na konci souboru) přidejte následující kód. Tento kód zobrazí seznam kurzů souvisejících s instruktorem, když je vybrán instruktor.
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == ViewBag.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
Tento kód přečte Courses
vlastnost modelu zobrazení, aby se zobrazil seznam kurzů. Poskytuje také Select
hypertextový odkaz, který odešle ID vybraného kurzu do Index
metody akce.
Spusťte stránku a vyberte instruktora. Teď uvidíte mřížku, která zobrazuje kurzy přiřazené vybranému instruktorovi a pro každý kurz uvidíte název přiřazeného oddělení.
Za blok kódu, který jste právě přidali, přidejte následující kód. Zobrazí se seznam studentů, kteří jsou zaregistrovaní v kurzu při výběru tohoto kurzu.
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
Tento kód přečte Enrollments
vlastnost modelu zobrazení, aby se zobrazil seznam studentů zaregistrovaných v kurzu.
Spusťte stránku a vyberte instruktora. Pak vyberte kurz, abyste viděli seznam zaregistrovaných studentů a jejich známek.
Přidání explicitního načítání
Otevřete InstructorController.cs a podívejte se, jak Index
metoda získá seznam registrací pro vybraný kurz:
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Když jste načetli seznam instruktorů, zadali jste dychtivé načítání pro Courses
navigační vlastnost a pro Department
vlastnost každého kurzu. Pak kolekci Courses
vložíte do modelu zobrazení a teď přistupujete k Enrollments
navigační vlastnosti z jedné entity v této kolekci. Protože jste nezadali dychtivé načítání navigační Course.Enrollments
vlastnosti, data z této vlastnosti se zobrazují na stránce v důsledku opožděného načítání.
Pokud jste zakázali opožděné načítání bez změny kódu jiným způsobem, Enrollments
vlastnost by byla null bez ohledu na to, kolik registrací kurz skutečně měl. V takovém případě byste kvůli načtení Enrollments
vlastnosti museli zadat buď dychtivé načítání, nebo explicitní načtení. Už jste viděli, jak načíst dychtivě. Abyste viděli příklad explicitního načtení, nahraďte Index
metodu následujícím kódem, který explicitně načte Enrollments
vlastnost. Změněný kód se zvýrazní.
public ActionResult Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(
i => i.ID == id.Value).Single().Courses;
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
// Lazy loading
//viewModel.Enrollments = viewModel.Courses.Where(
// x => x.CourseID == courseID).Single().Enrollments;
// Explicit loading
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
db.Entry(enrollment).Reference(x => x.Student).Load();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
Po získání vybrané Course
entity nový kód explicitně načte navigační vlastnost daného kurzu Enrollments
:
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
Pak explicitně načte související Student
entitu každé Enrollment
entity:
db.Entry(enrollment).Reference(x => x.Student).Load();
Všimněte si, že metodu Collection
používáte k načtení vlastnosti kolekce, ale pro vlastnost, která obsahuje pouze jednu entitu, použijete metodu Reference
.
Spusťte stránku indexu instruktora a neuvidíte žádný rozdíl v tom, co se na stránce zobrazuje, i když jste změnili způsob načtení dat.
Získání kódu
Další materiály
Odkazy na další prostředky Entity Framework najdete v ASP.NET přístupu k datům – doporučené zdroje informací.
Další kroky
V tomto kurzu se naučíte:
- Naučili jste se načíst související data.
- Vytvoření stránky Kurzy
- Vytvoření stránky instruktorů
V dalším článku se dozvíte, jak aktualizovat související data.