프레임워크 & 라이브러리/라라벨

[Laravel] 라라벨 데이터베이스와 모델 (쿼리빌더, 옐로퀸트)

라라벨에서 쿼리를 작성하는 방법은 간단하다. 레거시 PHP 에서 쿼리를 작성하는 방법은 PDO(PHP Data Object)를 사용하더라도 그다지 보기 좋은 것은 아니었다. 예를 들자면,

$pdo = new PDO(...);

$sth = $pdo->prepare("SELECT * FROM users");

if ($sth->execute()) {
	$users = [];
	while ($user = $sth->fetchObject()) {
		array_push($users, $user);
	}
}

이렇게 생긴 기존의 레거시 코드(mysqli, mysql 과 같은 더 low Level API 를 사용하면 보기가 안 좋아진다.)를 라라벨의 쿼리빌더를 다음과 같이 간단하게 표현할 수 있다.

$users = DB::table('users')->get();

더 나아가 옐로퀸트 ORM 을 사용하면 모델(User 클래스와 users 테이블이 연결된)과 조합하여 사용한다면 더 간단하게 표현하는 것도 가능하다.

$users = User::all();

레거시를 사용했을 때는 준비, 실행, 가져오기 과정을 PDO 를 통해 반복해야 했지만, 라라벨에서 제공하는 쿼리빌더, 예로퀸트를 통해 더욱 간단해질 수 있다.

쿼리빌더(Query Builder)

쿼리빌더는 라라벨에서 쿼리를 작성하기 위한 도구라고 볼 수 있다. 별도의 패키지라기 보다는 라라벨에 내장되어 있어서 언제든 사용할 수 있다. DB 파사드를 사용하면 간단하게 사용할 수 있다. 간단한 호출부터 살펴보자.

insert(), update(), select(), delete()

흔히 말하는 CRUD(Create, Read, Update, Delete)에 해당하는 작업들은 직접 쿼리를 적는것과 사실 큰 차이가 없다. 다만 PDO 를 사용하지만, 내부에서 준비과정이 발생하므로 명시적으로 쿼리를 준비할 필요는 없다는 점이다.

// INSERT
DB::insert("INSERT INTO users VALUES (:id)", ['id' => 1]);
// SELECT
DB::select("SELECT * FROM users WHERE id = :id", ['id' => 1]);
// UPDATE
DB::update("UPDATE users SET name = :name WHERE id = :id", ['name' => 'SangWoo', 'id' => 1]);
// DELETE
DB::delete("DELETE FROM users WHERE id = :id", ['id' => 1]);

이는 가장 기본적으로 사용할 수 있는 형태이지만 잘 사용되지는 않는다. 일반적으로는 모델을 통해 쿼리를 만들어 보내기 때문이다.

체이닝

메서드 체이닝을 하듯 쿼리빌더도 체이닝하여 사용할 수 있다.  위에 나왔던 코드는 더 예쁘게 다음과 같이 사용할 수도 있다.

// INSERT
DB::table('users')->insert(['id' => 1]);
// UPDATE
DB::table('users')->where('id', 1)->update(['name' => 'SangWoo']);
// SELECT
DB::table('users')->where('id', 1)->get();
// DELETE
DB::table('users')->where('id', 1)->delete();

위에서 where() 를 썼는데, where()->where() 처럼 연속으로 쓰면 AND 가 되고, orWhere() 를 쓰면 OR 처리가 된다. where() 이외에도 groupBy(), orderBy(), having() 등 사용할 수 있어서 유연하게 호출하여 쿼리를 만들어낼 수 있다. 이 글에서 쿼리빌더에 있는 메서드를 나열하는 것은 불필요하기 때문에 API 문서를 참고해보자.

 

https://laravel.com/api/8.x/Illuminate/Database/Query/Builder.html

$query->when(Closure)

해당 메서드를 사용하게 되면 조건에 따라 쿼리 포함 여부를 결정할 수 있다. 아래의 코드는 $isOnwer 가 참이면 클로저에 정의된 쿼리를 포함시킨다.

DB::table('users')->when($isOnwer, function ($query) {
	return $query->where('onwer', $isOnwer);
});

원시

추가적으로, 쿼리를 원시형태로 하려면 DB::raw() 를 사용하면 되고, DB::selectRaw() 처럼 사용할 수도 있으나 보안에 문제가 발생할 가능성이 있기때문에 사용에 각별히 유의해야 한다.

DB::raw("SELECT * FROM users");

트랜잭션

트랜잭션은 쿼리 여러 개를 묶은 일련의 과정이므로 작업의 단위로써 사용될 수 있다. 트랜잭션 내부에 쿼리를 사용하면서도 포함된 쿼리가 하나라도 실패한 경우, 이전에 실행한 것 또한 롤백(Rollback)된다.

DB::transaction(function () {
	// ...
});

// Or

DB::beginTransaction();

//
if (false) {
	DB::rolllback();
}

DB::commit();

이렇게 사용하거나 DB::beginTransaction() 을 시작으로 실패시 DB::rollback() 으로 이전에 작업한 것들을 되돌리거나, 성공시 DB::commit() 를 명시적으로 호출하여 사용할 수 있다.

모델 & 옐로퀸트(Eloquent)

옐로퀸트는 라라벨에서 제공하는 ORM(Object Relational Mapping)이다. 데이터베이스 테이블에 대응하는 모델(Model)의 프로퍼티에 매핑되는 액티브레코드 ORM 이다. 예를 들어 데이터베이스에 contracts 테이블이 있다면, 모델은 아래와 같이 생성할 수 있다. 모델은 라라벨에서 데이터베이스 테이블에 대응하는 클래스하고 생각하면 편하다. 모델을 생성할 때는 아티즌의 도움을 받을 수 있다.

php artisan make:model Contract

이렇게 생성된 모델은 관례에 따라 contracts 테이블에 연결되며 이후 $table 프로퍼티를 따로 설정하여 임의로 테이블의 이름을 바꿀 수 있으며 해당 모델을 통해 contracts 테이블의 레코트에 할 수 있는 추가, 갱신 등의 작업들을 처리할 수 있게 된다.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Contract extends Model {}

이렇게 만들어진 모델은 컨트롤러 내부에서 직접 쿼리빌더처럼 사용할 수 있으며 파사드처럼 호출하는 메서드를 호출하는 것이 가능하다.

// INSERT
Contract::create([
	'name' => 'SangWoo' 
]);

// INSERT
$contract = new Contract([
	'name' => 'SangWoo'
]);
$contract->save();

// UPDATE
$contract = Contract::find(1);
$contract->update(['name' => '상우']);

// SELECT
$contract = Contract::findOrFail(1);

// DELETE
$contract->delete();

모델 클래스의 이름을 직접 사용하여 파사드처럼 쓰거나 객체를 만들고 메서드를 호출하여 사용할 수 있다. 쿼리빌더에서 사용가능한 대부분의 메서드를 옐로퀸트 모델에서도 호출할 수 있다. get(), where() 와 같은 것들 말이다. 위와 같은 형태로 사용하려면 마이그레이션(Migration)이나 수동적으로 contracts 테이블을 미리 생성해두어야 하며 모델의 $fillable 프로퍼티에 name 속성이 할당 가능하도록 만들어야 한다.

 

https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html

스코프(Scoped)

모델이나 DB 파사드를 사용하여 조회할 때 where() 를 사용하여 데이터의 범위를 제한했었는데, 스코프 기능을 사용하면 각 쿼리마다 where() 로 처리해주어야 했던 것들을 간단하게 처리할 수 있다. 예를 들면 nameSangWoo Jeong 인 레코드만을 얻어오려면 다음과 같이 한다.

$contracts = Contract::where('name', 'SangWoo')->get();

이 예제는 간단하니까 상관없지만, 복잡한 조건이 달려있는 옐로퀸트 모델 객체를 얻어오기 위해 매번 저렇게 써야한다면 코드가 지저분해 질 것이다. 그래서 라라벨에선 로컬 스코프(Local Scope)글로벌 스코프(Global Scope)기능이 있다. 로컬 스코프를 사용하면 다음과 같이 해볼 수 있다.

$contracts = Contract::sangWoo()->get();

로컬 스코프

위와 같이 사용하고 싶다면 다음과 같이 모델에 스코프를 정의해보자. 이렇게 사용하면 복잡한 조건을 가진 모델을 조회할 때 보다 간단하게 처리해줄 수 있다.

class Contract extends Model
{    
    public function scopeSangWoo($query)
    {
        return $query->where('name', 'SangWoo');
    }
}

글로벌 스코프

글로벌 스코프를 사용하면 명시적으로 스코프에 해당하는 메서드를 호출하지 않더라도 해당 모델에 대해 처리할 때 무조건 스코프가 적용된다. 이 작업은 모델의 boot() 메서드에서 할 수 있다. 이 메서드는 모델들이 상속받는 Model 클래스에 이미 정의가 되어있고, 우리는 이를 재정의하는 것이 아니라 확장을 할 것이기 때문에 부모의 메서드도 호출시켜준다.

class Contract extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('sangWoo', function (Builder $builder) {
            $builder->where('name', '=', 'SangWoo');
        });
    }
}

PHP 클래스 내부에서 사용할 수 있는 키워드는 parent, self, static 의 3개가 있는데, static 키워드의 3번째 쓰임새 중 하나는 늦은(Lazy) 정적 바인딩에 쓰이는 것이다. 이를 사용하면 상속관계에서 자식에서 재정의 되지 않은 메서드를 자식 객체에서 호출하더라도 스코프는 자식 객체가 된다. PHP: Static (정적 변수, 정적 메서드, 늦은 정적 바인딩)을 참고하자.

 

만약 글로벌 스코프를 적용하고 싶지 않다면 withoutGlobalScope() 를 호출한다.

$contracts = Contract::withoutGlobalScope('sangWoo')->get();

모델과 옐로퀸트 부분은 아직 이야기하기 기능이 제법 많다. 접근자, 변경자, 관계 설정 등의 기능이 있다. 그러한 것들은 다음 포스트에서 알아보자.

더 읽을거리

https://laravel.com/docs/8.x/queries

https://laravel.com/docs/8.x/eloquent

https://laravel.kr/docs/8.x/queries

https://laravel.kr/docs/8.x/eloquent