# SqlRepository — מעבר מ-InMemory ל-EF Core📖 שיעור

## מה זה?

ביחידת ASP.NET Core בנינו תבנית שלמה: `IRepository<T>` (הבטחה ל-CRUD), `InMemoryStudentRepository` (מימוש שמחזיק `List<Student>` בזיכרון), `StudentService` (לוגיקה עסקית, מקבל `IRepository<Student>` בקונסטרוקטור), ו-`StudentsController` (מקבל `StudentService` בקונסטרוקטור). בשיעור הקודם למדנו להרים SQL Server אמיתי ולדבר איתו דרך `DbContext` — אבל חיברנו אותו ישירות ל-Controller, בעקיפין לכל התבנית הזו, רק כדי להוכיח שהחיבור הטכני עובד.

השיעור הזה סוגר את המעגל: כותבים `SqlStudentRepository` — מימוש **חדש** של **אותו** `IRepository<T>` שכבר קיים, עם `DbContext` במקום `List`. אם התבנית תוכננה נכון (וכבר ראינו שכן), `StudentService` ו-`StudentsController` לא ישתנו **אף שורה אחת**.

## מילות מפתח שחשוב לזכור

• `SqlRepository` — מימוש של `IRepository<T>` שמשתמש ב-EF Core (`DbContext`/`DbSet`) במקום מבנה נתונים בזיכרון

• Change Tracking — המנגנון שבו `DbContext` "זוכר" אילו אובייקטים נוספו/שונו/נמחקו, כדי לדעת איזו שאילתת SQL לייצר כש-`SaveChanges()` נקרא

• `appsettings.json` — קובץ ההגדרות של הפרויקט (כבר הוזכר בשיעור הראשון של היחידה הקודמת); כאן נשתמש בו בפועל לראשונה, במקום לכתוב מחרוזת חיבור ישירות בקוד

## בונים את SqlRepository שלב אחר שלב

### שלב 1: מזכירים את מה שכבר קיים

לפני שכותבים שורת קוד אחת, שני תזכורות קצרות מהיחידה הקודמת. ההבטחה שכבר הוגדרה:

```csharp
public interface IRepository<T>
{
    List<T> GetAll();
    T GetById(int id);
    void Add(T item);
    void Update(T item);
    void Delete(int id);
}
```

והמימוש הקיים, שמחזיק הכול בזיכרון:

```csharp
public class InMemoryStudentRepository : IRepository<Student>
{
    private List<Student> _students = new List<Student>();

    public List<Student> GetAll() => _students;
    public Student GetById(int id) => _students.FirstOrDefault(s => s.Id == id);
    public void Add(Student item) => _students.Add(item);
    public void Update(Student item) { /* update in the list */ }
    public void Delete(int id) => _students.RemoveAll(s => s.Id == id);
}
```

המטרה של השיעור הזה: לכתוב מחלקה **שלישית** — לא נוגעים בשתיים האלה — שמממשת בדיוק את אותו `IRepository<Student>`, אבל עם `SchoolDbContext` מהשיעור הקודם במקום `List`.

### שלב 2: מתחילים מהמתודה הכי פשוטה — GetAll

```csharp
public class SqlStudentRepository : IRepository<Student>
{
    private readonly SchoolDbContext _context;

    public SqlStudentRepository(SchoolDbContext context)   // injected automatically — AddDbContext from the previous lesson
    {
        _context = context;
    }

    public List<Student> GetAll() => _context.Students.ToList();
}
```

`: IRepository<Student>` אומר שהמחלקה הזו מתחייבת לממש את כל חמש המתודות של ה-Interface — עדיין רק אחת כתובה, בכוונה. הקונסטרוקטור מקבל `SchoolDbContext` — בדיוק אותו `DbContext` שבנינו בשיעור הקודם, ובדיוק אותו עקרון של Constructor Injection שכבר מוכר: אתם לא כותבים `new SchoolDbContext(...)` בשום מקום, ASP.NET מעביר אותו אוטומטית כי הוא רשום ב-DI (עוד מעט נזכיר גם את שורת הרישום).

`GetAll()` מתורגם ל-`SELECT * FROM Students` — בדיוק כמו שכבר ראיתם בשיעור הקודם.

### שלב 3: מוסיפים GetById

```csharp
public Student GetById(int id) => _context.Students.FirstOrDefault(s => s.Id == id);
```

זהה לחלוטין לתחביר ב-`InMemoryStudentRepository` משלב 1 — `FirstOrDefault` עובד זהה על `List<T>` ועל `DbSet<T>`. ההבדל היחיד (בלתי-נראה מהקוד עצמו): כאן זה מתורגם ל-`SELECT TOP(1) * FROM Students WHERE Id = @id`, לא לולאה על רשימה בזיכרון.

### שלב 4: מוסיפים Add

```csharp
public void Add(Student item)
{
    _context.Students.Add(item);
    _context.SaveChanges();   // this is where it's actually written to the DB
}
```

כאן מופיע ההבדל האמיתי הראשון מול `InMemoryStudentRepository`: שם, `_students.Add(item)` לבדו היה מספיק — השורה **היא** הפעולה. כאן, `_context.Students.Add(item)` **לא** כותב כלום ל-DB עדיין — הוא רק אומר ל-`DbContext` "תזכור שצריך להוסיף את זה" (זה בדיוק ה-Change Tracking שהוזכר במילות המפתח). רק `SaveChanges()` הופך את זה לשאילתת `INSERT` אמיתית. בלי השורה הזו, `Add` "עובד" מבחינת הקומפיילר אבל לא עושה כלום בפועל ב-DB.

### שלב 5: מוסיפים Update — מושג חדש, Change Tracking

```csharp
public void Update(Student item)
{
    _context.Students.Update(item);
    _context.SaveChanges();
}
```

`_context.Students.Update(item)` מסמן את האובייקט `item` כ"השתנה" אצל ה-`DbContext` — כך ש-`SaveChanges()` ידע לייצר `UPDATE` SQL (עם כל העמודות) במקום `INSERT`. זה שונה מהותית מ-`InMemoryStudentRepository`, ששם `Update` היה צריך למצוא את השורה הישנה ברשימה ולהחליף אותה ידנית — כאן `DbContext` עושה את ה"זיהוי מה השתנה" בשבילכם.

### שלב 6: מוסיפים Delete — המתודה האחרונה

```csharp
public void Delete(int id)
{
    var student = GetById(id);
    if (student != null)
    {
        _context.Students.Remove(student);
        _context.SaveChanges();
    }
}
```

שימו לב: קודם שולפים את האובייקט עם `GetById` (המתודה שכבר כתבנו בשלב 3) — EF Core לא יכול למחוק "לפי מספר בלבד", הוא צריך אובייקט שהוא כבר "מכיר" (עקב ה-Change Tracking). `if (student != null)` מונע ניסיון מחיקה של תלמיד שלא קיים בכלל. `Remove` + `SaveChanges()` — אותו דפוס בדיוק כמו שאר המתודות.

כל חמש המתודות של `IRepository<Student>` ממומשות — ה-`SqlStudentRepository` שלם.

### שלב 7: רושמים אותו ב-Program.cs — שורה אחת

```csharp
// before:
builder.Services.AddScoped<IRepository<Student>, InMemoryStudentRepository>();

// after — a single change:
builder.Services.AddScoped<IRepository<Student>, SqlStudentRepository>();
```

זהו. לא שורה נוספת, לא שינוי בשום מחלקה אחרת — רק **מה** נרשם מול אותו `IRepository<Student>`. `AddDbContext<SchoolDbContext>(...)` מהשיעור הקודם כבר רשום ונשאר בדיוק כמו שהיה; `SqlStudentRepository` מקבל ממנו את ה-`DbContext` בדיוק כפי שכל Controller קיבל אותו קודם.

### שלב 8: מחרוזת החיבור "כמו שצריך" — appsettings.json

בשיעור הקודם כתבנו את מחרוזת החיבור ישירות בתוך `Program.cs`, כדי להתמקד בעיקר. עכשיו, כשהכול עובד, שווה לעשות את זה נכון: זוכרים את `appsettings.json` מהשיעור הראשון של היחידה הקודמת ("קובץ הגדרות תצורה")? זה בדיוק המקום שמחרוזות חיבור אמורות לחיות בו:

```json
// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost,1433;Database=SchoolDb;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True;"
  }
}
```

ו-ב-`Program.cs`, במקום המחרוזת הקשיחה:

```csharp
// before (previous lesson):
// var connectionString = "Server=localhost,1433;...";

// after:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<SchoolDbContext>(options =>
    options.UseSqlServer(connectionString));
```

`builder.Configuration` יודע לקרוא את `appsettings.json` אוטומטית (זו בדיוק ההתנהגות המובנית שהוזכרה כבר בשיעור הראשון של היחידה הקודמת). `GetConnectionString("DefaultConnection")` הוא קיצור נוח ל-`"ConnectionStrings:DefaultConnection"` בתוך הקובץ. למה זה עדיף על מחרוזת קשיחה בקוד: אפשר לשים `appsettings.Development.json` נפרד עם DB אחר לפיתוח, ו-`appsettings.json` ל-production — בלי לגעת בקוד בכלל, רק בקבצי הגדרות.

### שלב 9: ה-Controller חוזר להיות דק

בשיעור הקודם, `StudentsController` דיבר ישירות עם `SchoolDbContext` — זה היה מסומן במפורש כקיצור-דרך זמני. עכשיו, כשיש `SqlStudentRepository` תקין, חוזרים בדיוק לקוד ה-Controller המקורי מיחידת ה-DAL — **בלי שום שינוי**:

```csharp
public class StudentsController : ControllerBase
{
    private readonly StudentService _service;
    public StudentsController(StudentService service) => _service = service;

    [HttpGet]
    public IActionResult GetAll() => Ok(_service.GetAll());

    [HttpPost]
    public IActionResult Create([FromBody] Student newStudent)
    {
        var student = _service.RegisterStudent(newStudent.Name, newStudent.Age);
        return CreatedAtAction(nameof(GetAll), student);
    }
}
```

זו בדיוק המחלקה מיחידת ה-DAL, מילה במילה. `StudentService` עצמה גם היא לא השתנתה — היא עדיין מקבלת `IRepository<Student>` בקונסטרוקטור, ולא יודעת (ולא צריכה לדעת) שמאחורי הקלעים זה כבר לא `List` בזיכרון אלא SQL Server אמיתי.

### שלב 10: מריצים ובודקים שבאמת כלום לא "הרגיש" את השינוי

```bash
dotnet run
```

```bash
curl -X POST http://localhost:5123/api/students -H "Content-Type: application/json" -d "{\"name\":\"Dana\",\"age\":25}"
curl http://localhost:5123/api/students
```

אותן שתי קריאות **בדיוק** כמו בסוף השיעור הקודם, אותה תוצאה — אבל הפעם הבקשה עוברת דרך `StudentsController → StudentService → SqlStudentRepository → DbContext → SQL Server`, לא דרך Controller שמדבר עם ה-DB ישירות. אם עוצרים את השרת ומריצים שוב, דנה עדיין שם — בדיוק כמו קודם, רק שעכשיו זה עובר דרך כל השכבות שבנינו ביחידה הקודמת, בדיוק כמו שתוכננו.

## הסבר עיקרי

זו ההוכחה המעשית ל-DIP ולתבנית ה-DAL כולה — `StudentService` מקבל `IRepository<Student>` דרך הקונסטרוקטור, ולא יודע (ולא צריך לדעת) אם המימוש בפועל הוא `List` בזיכרון או `DbContext` אמיתי. כל מה שהשתנה הוא **מה** נרשם ב-DI Container — שורה אחת ב-`Program.cs`.

`SaveChanges()` בכל מתודה, לא רק ב-`Add` — בניגוד ל-`InMemoryRepository` שבו `Add`/`Update`/`Delete` פעלו מיידית על ה-`List` בזיכרון, כאן כל שינוי חייב `SaveChanges()` מפורש כדי להיכתב בפועל ל-DB. זו נקודת ההבדל הכי משמעותית בין שני המימושים, למרות שה-Interface שלהם זהה לחלוטין.

## יתרונות

מעבר ממימוש בזיכרון למסד נתונים אמיתי הוא שינוי מבודד לחלוטין — הוכחה חיה לערך של DIP+DAL; קוד ה-Service וה-Controller נשארים זהים לגמרי, כולל בדיקות שכבר נכתבו עליהם; Connection String דרך Configuration מאפשר סביבות מרובות בלי שינוי קוד.

## חסרונות

`SqlRepository` דורש `SaveChanges()` מודע בכל מתודה — קל לשכוח בהשוואה למימוש InMemory ה"אוטומטי"; שגיאות DB אמיתיות (חיבור נכשל, timeout) הן מקור כשלים חדש שלא היה קיים במימוש InMemory כלל, ודורש טיפול נוסף.

## נקודות חשובות

• `SqlRepository` מממש את אותו `IRepository<T>` כמו `InMemoryRepository` — אותו חוזה, מימוש שונה

• השינוי היחיד הנדרש במעבר הוא רישום ה-DI ב-`Program.cs`, לא בכל קוד קורא

• כל מתודה ב-`SqlRepository` שמשנה נתונים חייבת `SaveChanges()` משלה; `Update`/`Remove` מסתמכים על Change Tracking

• Connection String נטען מ-`appsettings.json`/Configuration, לא מקושח בקוד

## טעויות נפוצות

• לשכוח `SaveChanges()` באחת המתודות (למשל `Delete`) ולתהות למה השינוי "לא נשמר"

• לקושש Connection String ישירות בקוד במקום Configuration — קשה להחליף בין סביבות (ראו שלב 8)

• לשנות בטעות את חתימת `IRepository<T>` כדי "להתאים" ל-EF Core — מאבד את כל התועלת של ההפשטה המקורית

## סיכום

`SqlRepository` הוא מימוש חדש של אותו `IRepository<T>` שכבר הוגדר ביחידת ASP.NET Core, הפעם עם EF Core (`DbContext`/`DbSet`) במקום `List` בזיכרון. בנינו אותו מתודה-מתודה (`GetAll` → `GetById` → `Add` → `Update` → `Delete`), רשמנו אותו ב-`Program.cs` בשורה אחת, העברנו את מחרוזת החיבור ל-`appsettings.json`, והחזרנו את ה-Controller לגרסתו הדקה המקורית — בלי לגעת בשכבות ה-Service וה-Controller כלל. זו ההוכחה המעשית והמוחשית ביותר לערך של הפרדת שכבות שנלמדה קודם.

## דוקומנטציה רשמית

[Microsoft Learn — Implementing the repository pattern with EF Core](https://learn.microsoft.com/ef/core/miscellaneous/configuring-dbcontext)

[Microsoft Learn — Configuration in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/configuration/)

---

## תרגילים

### תרגיל 1 — SqlRepository ראשון

**המשימה:** ממשו `SqlProductRepository : IRepository<Product>` מתרגיל היחידה הקודמת, עם `DbContext` אמיתי במקום `List` — מתודה אחר מתודה, כמו בשלבים 2-6 למעלה.

**בדיקה:** כל פעולות ה-CRUD עובדות זהה מבחינת ה-Controller/Service, אך הנתונים נשמרים בפועל ב-DB (נשארים גם אחרי restart של האפליקציה).

### תרגיל 2 — החלפת רישום DI

**המשימה:** קחו פרויקט קיים עם `InMemoryProductRepository` רשום ב-DI, והחליפו לרישום `SqlProductRepository` — שינוי בשורה אחת בלבד.

**בדיקה:** כל שאר הקוד (Service, Controller) ממשיך לעבוד בלי שום שינוי נוסף.

---

## פרויקט מסכם

**המשימה:** השלימו את המעבר המלא של `TasksController`/`TaskService` (מהיחידה הקודמת) ל-EF Core אמיתי.

**דרישות:**
1. `TaskDbContext` עם `DbSet<TaskItem>`, ו-Migration ראשונה
2. `SqlTaskRepository : IRepository<TaskItem>` עם מימוש מלא (כל מתודה עם `SaveChanges()` משלה)
3. עדכון `Program.cs` בלבד (כולל מעבר ל-`appsettings.json`) — `TaskService` ו-`TasksController` הקיימים לא נוגעים

**בדיקה:** כל ה-endpoints הקיימים (CRUD מלא) ממשיכים לעבוד זהה מבחוץ; יצירת משימה ואז restart של האפליקציה + `GET` מראה שהמשימה עדיין קיימת — הוכחה שהיא נשמרה ב-DB אמיתי, לא רק בזיכרון.

## מה בפרק הבא

בפרק הבא — **Queries מתקדמות**: LINQ על `DbSet`, ההבדל בין raw SQL ל-LINQ, `Include` לטעינת קשרים (`Student.Grades` מהשיעור על DbContext), ובעיית ה-N+1 — מלכודת ביצועים נפוצה שחייבים להכיר.
