티스토리 뷰

지난 포스트에서 HTTP 메소드를 만들어 모델에 대해 CRUD를 했다.

 

Fluent의 query API는 데이터베이스에서 모델을 CRUD할 뿐만아니라 필터링, 조인, 청크, 집계 등을 지원한다.

 

이번 글에서는 Fluent 공식 문서에 있는 쿼리 API들을 살펴보자

 

 

[Vapor] Swift로 서버 만들기 06 - Fluent에 HTTP 메소드 만들기 (GET, POST, PUT, DELETE)

지난 포스트에서 Fluent와 PostgreSQL을 연결하고 URL을 통해 그 결과를 확인했다. 이번 글에서는 기본으로 제공되는 메소드를 분석해보고, 추가로 메소드를 작성해 CRUD를 할 것이다. [Vapor] Swift로 서

malchafrappuccino.tistory.com

 

 

새로운 Controller 만들기

기존 TodoController말고 새로운 Controller를 만들어서 사용하자. 이름은 TodoQueryController로 했다.

 

+ 파일 만들때 import Fluent, Vapor 꼭 해주자. 에러 났는데 import 안해줘서 난 에러였다....

 

import Fluent
import Vapor

struct TodoQueryController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let todoQuery = routes.grouped("todoQuery")
    }
}

 

RouteCollection을 채택하고 boot 함수를 구현했다.

 

route 함수에 register를 해준다.

func routes(_ app: Application) throws {
	// ...
   
    try app.register(collection: TodoController())
    try app.register(collection: TodoQueryController())
}

 

이제 http://127.0.0.1:8080/todoQuery 를 통해 접근할 것이다.

 

 

ALL

all() 메소드는 데이터베이스에 있는 모든 모델을 배열로 리턴한다.

struct TodoQueryController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let todoQuery = routes.grouped("todoQuery")
        
        todoQuery.get("all", use: queryAll)
    }
    
    func queryAll(req: Request) async throws -> [Todo] {
        try await Todo.query(on: req.db).all()
    }
}

path에 all을 추가하고 get 메소드를 추가해준다.

 

http://127.0.0.1:8080/todoQuery/all

 

GET을 날려주면

모델이 모두 리턴되는 것을 확인할 수 있다.

 

all() 메소드에서 원하는 Field만 가져올 수도 있다.

 

struct TodoQueryController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let todoQuery = routes.grouped("todoQuery")
        
     	//...
        todoQuery.get("allTitle", use: queryAllTitle)
    }
    
    //...
    
    // title은 String이므로 리턴타입 바꿔줘야함!
    func queryAllTitle(req: Request) async throws -> [String] {
        try await Todo.query(on: req.db).all(\.$title)
    }
}

 

all() 메소드 안에 Field를 인자로 넣어주고 호출하면 된다. title만 받아와보자

 

 

제목만 받아볼 수 있다.

 

 

Filter (필터링)

해당하는 값만 필터링해서 리턴해줄 수 도 있다. Value, Field, Subset, Contains, Group이 있는데 하나씩 살펴보자

 

 

Value Filter

가장 많이 쓰이는 필터로 원하는 값을 추출한다.

 

filter() 메소드 안에 인자를 넣어준다. 왼쪽에는 Field 값 == 오른쪽에는 필터링하고 싶은 값을 넣어준다.

 

title로 모델을 찾는 함수를 만들어보자

 

func queryFilterTitle(req: Request) async throws -> [Todo] {
    let targetTitle = req.parameters.get("todoTitle") ?? ""
    return try await Todo.query(on: req.db).filter(\.$title == targetTitle).all()
}

 

title을 인자로 받아 해당하는 모든 모델을 배열로 리턴하는 함수이다.  title은 고유값이 아니기 때문에 중복될 수 있으므로 배열로 리턴해주었다.

 

 

boot함수 안에 title을 인자로 받는 함수를 작성해준다.

todoQuery.group(":todoTitle") { todo in
    todo.get(use: queryFilterTitle)
}

 

 

이렇게 작성하면 아래 주소로 메소드를 호출할 수 있다.

http://127.0.0.1:8080/todoQuery/{title 넣는 곳}

 

Postman에서 확인해보면

 

해당하는 값이 잘 리턴된다.

 

 

참고로 왼쪽, 오른쪽 바꾸면 에러나니 꼭 왼쪽에 Field, 오른쪽에 필터링 값(value)을 넣어주자

 

 

연산자는 총 6가지 있는데 모두가 다 아는 기본 연산자이다. ->  ==, !=, >=, >, <, <=

 

 

Filed Filter (필드)

필드끼리 비교한다. 연산자는 Value Filter와 마찬가지로 6가지를 지원한다.

 

title과 description이 같은 모델을 찾는 메소드를 작성해보자.

 

func queryFilterField(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).filter(\.$title == \.$description).all()
}

 

현재 DB에 제목과 내용이 같은 모델이 없어서 따로 확인하지는 않겠다.

 

 

Subset Filter

Subset에 해당하는 모델을 필터해준다. 2개의 연산자가 있다.

 

 

~~ set에 해당하는 값
!~ set에 해당하지 않는 값

 

코드로 확인하는게 이해가 빠르다.

func queryFilterSubset(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).filter(\.$title ~~ ["이의 있소!", "AWS 계정 만들기"]).all()
}

 

우측 Subset ["이의 있소!", "AWS 계정 만들기"에 해당하는 모델만 리턴한다. Postman으로 확인하면

 

 

해당하는 값들을 확인할 수 있다.

 

!~ 연산자를 사용한다면 2개를 제외한 값들이 나올 거다.

 

이렇게

 

 

Contain Filter (부분문자열 포함 여부)

Field에 해당하는 부분 문자열(Substring)이 있는지 확인한다. 똑같이 filter()를 사용하는데 연산자가 다르다.

 

 

~~ 부분 문자열이 있다
!~ 부분 문자열이 없다.
=~ prefix 일치
!=~ prefix 불일치
~= suffix 일치
!~= suffix 불일치

 

Subset Filter와 다른 점은 Subset은 우측에 배열(=Subset)이 들어가고, Contain은 문자열이 들어간다.

 

func queryFilterContain(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).filter(\.$title ~~ "하기").all()
}

제목에 하기가 포함되는 모델을 다 리턴한다.

 

하기가 포함된 모델들만 리턴된다.

 

 

Group

filter를 and, or 로직으로 묶어 여러 개 사용할 수 있다.

 

func queryFilterGroup(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).group(.or) { group in
        group.filter(\.$title == "이의 있소!").filter(\.$title == "도커 공부하기")
    }.all()
}

제목이 "이의 있소!" 이거나 "도커 공부하기" 인 모델을 모두 리턴하는 함수다.

 

왜 이렇게 했나 했더니 filter() 안에 or 연산자를 사용하면 에러가 난다.

결과를 확인해보면

 

 

or 연산자가 잘 작동한다.

 

 

Aggregate(집합)

count, sum과 같은 SQL 집합 함수들이다. 총 5가지가 있다.

 

 

count 카운트 함수
sum 값들의 합
average 평균값
min 최소값
max 최대값

 

DB 전체 count를 리턴하는 함수를 작성해보자

 

func queryCount(req: Request) async throws -> Int {
    try await Todo.query(on: req.db).count()
}

 

 

4개가 리턴된다.

 

sum, average, min, max 함수는 해당하는 필드를 인자로 넣어주면 된다.  -> 이 때 필드 값이 Int 타입이어야 한다.

 

Int 타입 아닐 때 에러

지금 예제는 제목이랑 내용 필드가 다 String 타입이라 확인이 불가능하다.

 

 

Chunk

집합을 별도의 청크(묶음)로 리턴할 수 있다.

 

제공된 클로저는 총 결과 수에 따라 0회 이상 호출된다. 반환된 각 항목은 모델 또는 데이터베이스 항목 디코딩을 시도하여 반환된 오류를 포함하는 결과이다.

 

func queryChunk(req: Request) async throws {
    try await Todo.query(on: req.db).chunk(max: 3, closure: { todo in
        // todo를 3개씩 다루기
    })
}

 

 

Field (필드)

filed() 를 사용해 모델에서 원하는 필드만 리턴해줄 수 있다.

 

제목과 내용만 리턴해주는 함수를 작성해보자

func queryFieldTitleDescription(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).field(\.$title).field(\.$description).all()
}

 

 

나머지 값은 다 null로 온다.

 

 

 

Unique (중복 값 제거)

중복되지 않는 값을 리턴한다.

 

all() 안에 원하는 필드를 넣어준다.

func queryFieldUnique(req: Request) async throws -> [String] {
    try await Todo.query(on: req.db).unique().all(\.$description)
}

 

중복되지 않는 내용만 리턴한다.

 

중복된 값이 있으면 아예 리턴이 안되는 건가?? 확인을 위해 "AWS는 어렵다"라는 내용을 가지는 모델을 하나 더 추가해줬다.

 

하고 유니크를 다시 호출하면

 

"AWS는 어렵다" 가 나온다. -> SQL 문법 DISTINCT를 생각하면 된다.

 

 

Range (범위)

swift 범위 연산자를 사용해 해당하는 범위의 모델만 리턴해준다.

 

func queryRange(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).range(..<3).all()
}

 

 

데이터베이스는 순서가 없어서 요청할 때마다 다른 값을 리턴할 줄 알았는데, 계속 저장된 순으로 리턴해준다. 더 확인해야겠지만 안전하게 sort()와 같이 사용하는게 좋아보인다.

 

 

Paginate (페이지네이션)

페이지네이션 기능을 사용해 페이지 별로 불러올 수 있다. pageper이라는 metadata 데이터를 사용한다.

 

  • page : 해당 페이지
  • per : page에서 불러올 모델의 개수

 

paginate() 를 사용하면 된다. 이 때 모델을 Page<>로 감싼 값을 리턴해줘야 한다.

func queryPaginate(req: Request) async throws -> Page<Todo> {
    try await Todo.query(on: req.db).paginate(for: req)
}

 

metadata 를 Params에 넣어준다. 요청은 아래와 같다.

http://127.0.0.1:8080/todoQuery/paginate?page=1&per=2

 

2개씩 불러오는데 그 중 1번 페이지를 불러온다.

 

2개의 데이터와 함께 metadata가 함께 도착했다. 프론트에서는 metadata를 다루기 위한 모델을 추가해주어야 한다.

 

 

Sort (정렬)

sort() 안에 정렬하고 싶은 필드 값을 넣어준다. 생성 순으로 리턴해주는 함수를 작성해보자

func querySort(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).sort(\.$createdAt).all()
}

 

 

 

 

이번엔 역순으로 리턴하는 함수를 작성해보자. sort() 에서 따로 설정하는 게 없어서, 정렬된 값을 reversed() 를 사용해 뒤집어주자

func querySort(req: Request) async throws -> [Todo] {
    try await Todo.query(on: req.db).sort(\.$createdAt).all().reversed()
}

 

 

거꾸로 도착 완료

 

 

Join

현재 Todo 예제에는 조인할 다른 테이블이 없다. 공식 문서에 있는 예제 코드를 살펴보자

Planet.query(on: database)
    .join(Star.self, on: \Planet.$star.$id == \Star.$id)
    .filter(Star.self, \.$name == "Sun")
    .all()

Planet 테이블과 Star 테이블을 조인하고, name이 "Sun"인 모델을 모두 리턴한다.

 

SQL 문과 거의 유사하게 생겼다. SQL 문으로 쓰면 아래와 같다.

SELECT * FROM PLANET p JOIN STAR s ON p.id = s.id WHERE s.NAME = "SUN"

 

 

정리

Fluent의 공식 문서에 있는 필터, 정렬, 페이지네이션 등의 쿼리를 예제에 적용해보았다. 

 

이전에 SQL 코테를 준비하느라 SQL 문법을 공부했어서 그런지 익숙했다.  SQL 쓸 일 없을 줄 알았는데 이렇게 쓰이네 ㅎㅎ

 

예제 코드들은 아래 Gist에서 확인할 수 있다.

 

 

TodoQueryController.swift

GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

쿼리 끝!

 

 

다음 포스트 - Vapor 프로젝트 도커 배포하기

 

 

 

[Vapor] Swift로 서버 만들기 08 - 도커(Docker)로 배포하기

이번 글에서는 Vapor 프로젝트로 도커 컨테이너를 만들어볼 것이다. 도커를 몰라도 괜찮다. Vapor에서 기가 막히게 도커 배포를 잘 지원해주고 있다. 공식 문서를 보면서 한 번 만들어보자 [Vapor] Swi

malchafrappuccino.tistory.com

 

 

출처

 

Vapor: Fluent → Query

Query Fluent's query API allows you to create, read, update, and delete models from the database. It supports filtering results, joins, chunking, aggregates, and more. // An example of Fluent's query API. let planets = try await Planet.query(on: database)

docs.vapor.codes

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함