CRUD 구현
담고 읽고 고치고 지웁니다.
2부의 주소에 3부의 데이터베이스를 붙입니다
2.5에서 정한 주소 설계와 3.1·3.2의 EF Core 를 합칩니다. 달라지는 것은
List<Todo> 자리에
db.Todos 가 들어가는 것뿐입니다.
| 동작 | 주소 | EF Core |
|---|---|---|
| 읽기 | GET /todos | db.Todos.ToListAsync() |
| 하나 읽기 | GET /todos/1 | db.Todos.FindAsync(1) |
| 담기 | POST /todos | db.Todos.Add(…) + SaveChangesAsync() |
| 고치기 | PUT /todos/1 | 읽어서 속성을 바꾸고 SaveChangesAsync() |
| 지우기 | DELETE /todos/1 | db.Todos.Remove(…) + SaveChangesAsync() |
모두 비동기로 적습니다. 데이터베이스를 기다리는 동안 스레드를 붙잡고 있으면 그만큼 다른 요청을 받지 못합니다. C# 4.1에서 본 것이 여기서 쓰입니다.
컨트롤러 하나에 다섯 가지
[ApiController] [Route("todos")] public class TodosController(TodoContext db) : ControllerBase { [HttpGet] public async Task<IEnumerable<Todo>> List(CancellationToken token) => await db.Todos.AsNoTracking().ToListAsync(token); [HttpGet("{id:int}")] public async Task<ActionResult<Todo>> Get(int id) => await db.Todos.FindAsync(id) is { } found ? found : NotFound(); [HttpPost] public async Task<ActionResult<Todo>> Add(Todo todo, CancellationToken token) { db.Todos.Add(todo); await db.SaveChangesAsync(token); return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo); } [HttpPut("{id:int}")] public async Task<IActionResult> Update(int id, Todo input, CancellationToken token) { if (await db.Todos.FindAsync([id], token) is not { } found) return NotFound(); found.Title = input.Title; found.Done = input.Done; found.Priority = input.Priority; await db.SaveChangesAsync(token); return NoContent(); } [HttpDelete("{id:int}")] public async Task<IActionResult> Remove(int id, CancellationToken token) { if (await db.Todos.FindAsync([id], token) is not { } found) return NotFound(); db.Todos.Remove(found); await db.SaveChangesAsync(token); return NoContent(); } }
이 코드는 데이터베이스가 있어야 하므로 브라우저에서 실행할 수 없습니다.
CancellationToken 을 매개 변수로 두면 브라우저가
연결을 끊었을 때 질의도 함께 멈춥니다. 적지 않아도 동작하지만, 사용자가
화면을 닫은 뒤에도 데이터베이스가 계속 일하게 됩니다.
어떤 SQL 이 나가나
각 동작에서 실제로 나간 SQL 입니다.
// 담기 INSERT INTO "Todos" ("Done", "Priority", "Title") VALUES (@p0, @p1, @p2) RETURNING "Id"; // 고치기 — Done 만 바꾼 뒤 UPDATE "Todos" SET "Done" = @p0 WHERE "Id" = @p1 RETURNING 1; // 지우기 DELETE FROM "Todos" WHERE "Id" = @p0 RETURNING 1;
두 가지를 보십시오.
INSERT에 RETURNING "Id" 가 붙었습니다. 데이터베이스가 매긴 번호를 받아 우리 객체에 넣어 줍니다. 그래서SaveChanges뒤에todo.Id를 사용할 수 있습니다.UPDATE에 바꾼 컬럼 하나만 있습니다. Title 과 Priority 는 들어가지 않았습니다.
SaveChanges() 는 바뀐 줄 수를
돌려줍니다. 0이 돌아왔다면 아무것도 바뀌지 않은 것이므로, 고쳤다고 알리기 전에
확인할 자리로 쓸 수 있습니다.
Find 와 First 는 다릅니다
둘 다 하나를 가져오지만 Find 는 먼저 자기 기억을 봅니다. 이미 읽어 둔 것이면 SQL 을 보내지 않습니다.
await db.Todos.FindAsync(1); await db.Todos.FindAsync(1); await db.Todos.FirstAsync(t => t.Id == 1); await db.Todos.FirstAsync(t => t.Id == 1);
그래서 키로 하나를 찾을 때는 Find 가 낫습니다. 다만 기억에 있는
것을 돌려주므로, 그 사이에 다른 곳에서 바뀐 값은 보지 못합니다. 언제나 최신을
읽어야 한다면 First 를 사용하거나 컨텍스트를 새로
받습니다.
읽지 않고 한꺼번에 고치기
"끝나지 않은 것을 모두 끝냄" 처럼 여러 줄을 한 번에 바꿀 때, 위 방식대로 하면 줄을 모두 읽어 온 뒤 하나씩 UPDATE 합니다. 줄이 많으면 그만큼 느립니다.
var 고친수 = await db.Todos .Where(t => !t.Done) .ExecuteUpdateAsync(s => s.SetProperty(t => t.Done, true));
SELECT 가 없습니다. 읽지 않고 곧바로 고칩니다. 지우는
쪽은 ExecuteDeleteAsync() 입니다.
다만 이 방식은 지켜보는 기억을 거치지 않습니다.
SaveChanges 를 부르지 않고 곧바로 나가며, 이미 읽어 둔
객체가 있어도 그 값은 바뀌지 않습니다. 섞어 사용할 때는 컨텍스트를 새로 받는 편이
헷갈리지 않습니다.
두 사람이 같은 줄을 고치면
갑과 을이 같은 할일을 동시에 열어 두고 각각 저장했습니다. 결과가 무엇을 고쳤느냐에 따라 다릅니다.
갑 : Title = "갑이 고침" 을 : Priority = 9 // 결과 Title = 갑이 고침 Priority = 9
갑 : Title = "갑이 고침" 을 : Title = "을이 고침" // 결과 Title = 을이 고침 // 갑의 것은 사라졌습니다
왼쪽이 살아남은 것은 앞에서 본 UPDATE 가 바꾼 컬럼만 담기 때문입니다. 을의 UPDATE 에 Title 이 없어 갑의 것을 덮지 않았습니다.
오른쪽은 나중에 저장한 쪽이 이깁니다. 갑은 자기 것이 사라진
줄도 모릅니다. 이것을 막으려면 "내가 읽은 뒤로 바뀌었는지" 를 함께 보아야 합니다.
EF Core 에서는 [Timestamp] 나
IsConcurrencyToken() 으로 그 칸을 두고, 어긋나면
DbUpdateConcurrencyException 이 발생합니다.
ExecuteUpdate 와 ExecuteDelete
읽지 않고 한꺼번에 고치는 방법이 이때 들어왔습니다. 그 전에는 줄을 모두 읽어 와 하나씩 바꾸거나, SQL 을 직접 적어야 했습니다.
FindAsync 에 취소 토큰을 넘길 때 모양이
다릅니다. 첫 인수가 키 배열이므로
FindAsync([id], token) 입니다. 그냥
FindAsync(id, token) 이라고 적으면 토큰이
두 번째 키 값으로 읽혀 엉뚱한 예외가 발생합니다.
직접 해보기
PATCH /todos/1 로 끝냄 여부만 바꾸는 메서드를
적어보세요. 나머지 값은 건드리지 않습니다.
위 Remove 는 없는 번호에 404를 돌려줍니다. 그런데
2.5에서 DELETE 는 여러 번 불러도 결과가 같아야 한다고 했습니다.
둘이 어긋나는지 생각해보고, 어느 쪽이든 까닭을 적어보세요.
갑의 수정이 사라지지 않게 하려 합니다. 고치는 메서드에 무엇을 더해야 을이 저장할 때 막을 수 있을지 적어보세요.
- 주소 설계는 2.5 그대로이고
List자리에db.Todos가 들어갑니다. 모두 비동기로 적습니다. UPDATE에는 바꾼 컬럼만 들어갑니다.INSERT는 매겨진 번호를 돌려받습니다.Find는 기억을 먼저 보고,First는 언제나 SQL 을 보냅니다.- 여러 줄을 한꺼번에 고칠 때는
ExecuteUpdateAsync로 읽지 않고 고칩니다. - 같은 칸을 동시에 고치면 나중에 저장한 쪽이 이깁니다. 막으려면 바뀔 때마다 달라지는 칸을 두어야 합니다.