ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] ORM JOIN
    언어/파이썬 & 장고 2016. 10. 5. 13:50

    django에서 orm을 사용해 join을 하는 방법은 여러가지가 존재합니다. orm에서 제공하는 함수를 사용하기 위해선 먼저 테이블간 foreign key가 잡혀있어야만 가능합니다. 지금부터 foreign key를 논리적으로 잡는 방법 및 foreign key가 잡혀있을 때의 join과 foreign key가 존재하지 않을 때 join하는 방법을 소개하겠습니다.

    1. test case (sql)

    CREATE TABLE public."group"
    (
      id integer NOT NULL DEFAULT nextval('group_id_seq'::regclass),
      name text NOT NULL,
      age smallint,
      num integer,
      CONSTRAINT group_pkey PRIMARY KEY (id)
    )
     
    CREATE TABLE abcd.test
    (
      num serial NOT NULL,
      id text,
      id2 text,
      CONSTRAINT test_pkey PRIMARY KEY (num)
    )

    db는 postgresql이며 위와 같이 각기 다른 db서버 및 스키마에 2개의 테이블을 임시로 만들어 테스트를 진행하도록 하겠습니다. 

    2. model 정의

    논리적 관계

    1. table 정의를 확인해보면 group이라는 테이블은 public 스키마에 존재하고 test 테이블은 abcd라는 스키마에 존재합니다. 또한 foreign key가 걸려있지도 않으며 test 테이블의 id 컬럼은 unique로 constraint가 잡혀있지 않습니다.

    foreign key를 걸기 위해선 같은 db서버에 테이블이 존재해야 하며 unique 제약이 걸려있어야 합니다. 하지만 django의 model에서는 논리적으로 이를 해결할 수 있습니다.

    class Group(models.Model):
        id = models.AutoField(primary_key=True)
        name = models.TextField(blank=True, null=True)
        age = models.SmallIntegerField(blank=True, null=True)
        num = models.ForeignKey(RemoteTest, help_text='논리적 외래키 테스트')
        class Meta:
            managed = False
            db_table = 'group'
     
    class RemoteTest(models.Model):
        num = models.AutoField(primary_key=True)
        id = models.TextField(blank=True, null=True)
        id2 = models.TextField(blank=True, null=True)
    
        class Meta:
            managed = False
            db_table = 'remote_test'

    먼저 다른 db서버에 존재하는 test 테이블을 group 테이블이 존재하는 db서버 및 스키마로 postgres_fdw를 사용하여 정의합니다. (링크참조) 정의를 한 다음 위와같이 테이블 스키마를 정의합니다. 

    위와같이 num이라는 필드를 foreignkey로 변경하고 참조테이블 및 컬럼을 지정합니다. 참조되는 테이블의 컬럼이 pk가 아니라면 unique=True 속성을 추가해 줍니다.

    물론 unique=True로 지정해서 실행은 되지만 db내의 데이터가 unique가 아닐시 에러를 발생시킵니다.

    참조하는 대상이 pk인 경우

    먼저 join을 하기위해서 model에 대한 정의가 필요로 합니다.

    class Group(models.Model):
        id = models.AutoField(primary_key=True)
        name = models.TextField(blank=True, null=True)
        age = models.SmallIntegerField(blank=True, null=True)
        num = models.ForeignKey(RemoteTest, help_text='논리적 외래키 테스트')
        class Meta:
            managed = False
            db_table = 'group'
     
    class RemoteTest(models.Model):
        num = models.AutoField(primary_key=True)
        id = models.TextField(blank=True, null=True)
        id2 = models.TextField(blank=True, null=True)
    
        class Meta:
            managed = False
            db_table = 'remote_test'

    group이라는 table의 num 컬럼이 remote_test라는 table의 num 컬럼을 참조하고 있는 상황입니다. 위의 같이 어떠한 컬럼도 지정하지 않으면 기본값으로 참조하는 테이블의 pk를 참조합니다. remote_test의 pk값인 num을 잡고 있는 형태입니다. 이와 같은 경우엔 위와 같이 간단하게 정의할 수 있습니다.

    참조하는 대상이 pk가 아닌 경우

    class Group(models.Model):
        id = models.AutoField(primary_key=True)
        name = models.TextField(blank=True, null=True)
        age = models.SmallIntegerField(blank=True, null=True)
        num = models.ForeignKey(RemoteTest, to_field='id', db_column='num', help_text='논리적 외래키 테스트')
        class Meta:
            managed = False
            db_table = 'group'
     
    class RemoteTest(models.Model):
        num = models.AutoField(primary_key=True)
        id = models.TextField(blank=True, null=True)
        id2 = models.TextField(blank=True, null=True)
    
        class Meta:
            managed = False
            db_table = 'remote_test'

    pk가 아닌 다른 컬럼을 참조하고 싶은 경우 to_field를 추가하여 사용합니다. 이때 주의해야 할 점이 존재합니다.

    주의점

    1. to_field를 선언하지 않고 RemoteTest만 선언한 경우

      1. class Group(models.Model):
            id = models.AutoField(primary_key=True)
            name = models.TextField(blank=True, null=True)
            age = models.SmallIntegerField(blank=True, null=True)
            num = models.ForeignKey(RemoteTest, help_text='논리적 외래키 테스트')
            class Meta:
                managed = False
                db_table = 'group'
         
        class RemoteTest(models.Model):
            num = models.AutoField(primary_key=True)
            id = models.TextField(blank=True, null=True)
            id2 = models.TextField(blank=True, null=True)
        
            class Meta:
                managed = False
                db_table = 'remote_test'


      2. 위에서 얘기한 것과 같이 기본값으로 primary key를 잡기 때문에 num을 참조하게 됩니다.
        1.  위 모델의 inner join 결과

          SELECT "group"."id", "group"."name", "group"."age", "group"."num_id", "remote_test"."num", "remote_test"."id", "remote_test"."id2" FROM "group" INNER JOIN "remote_test" ON ( "group"."num_id" = "remote_test"."num" ) ORDER BY "group"."id" ASC


    2. to_field만 선언한 경우

      1. class Group(models.Model):
            id = models.AutoField(primary_key=True)
            name = models.TextField(blank=True, null=True)
            age = models.SmallIntegerField(blank=True, null=True)
            num = models.ForeignKey(RemoteTest, to_field='id', help_text='논리적 외래키 테스트')
            class Meta:
                managed = False
                db_table = 'group'
         
        class RemoteTest(models.Model):
            num = models.AutoField(primary_key=True)
            id = models.TextField(blank=True, null=True, unique=True)
            id2 = models.TextField(blank=True, null=True)
        
            class Meta:
                managed = False
                db_table = 'remote_test'


      2. to_field로 참조하는 컬럼을 가리키는 문제는 해결했지만 django에서 설정한 기본적인 문제 하나가 걸립니다. django에서 orm을 사용할 때, pk가 정의가 되어있지 않으면 기본으로 id라는 컬럼(id 컬럼이 정의되어 있지 않아도)을 pk라고 인식하고 찾습니다. 유사한 문제로 num이라는 컬럼이 pk가 아니기 때문에 'num_id'라고 임의로 지정해 on 절에서 비교를 하게 됩니다.
        1. 위 모델의 inner join 결과

          SELECT "group"."id", "group"."name", "group"."age", "group"."num_id", "remote_test"."num", "remote_test"."id", "remote_test"."id2" FROM "group" INNER JOIN "remote_test" ON ( "group"."num_id" = "remote_test"."id" ) ORDER BY "group"."id" ASC


    3. db_column을 잘못 정의한 경우 (또는 db_column만 정의한 경우)

      1. class Group(models.Model):
            id = models.AutoField(primary_key=True)
            name = models.TextField(blank=True, null=True)
            age = models.SmallIntegerField(blank=True, null=True)
            num = models.ForeignKey(RemoteTest, to_field='id', db_column='test', help_text='논리적 외래키 테스트')
            class Meta:
                managed = False
                db_table = 'group'
         
        class RemoteTest(models.Model):
            num = models.AutoField(primary_key=True)
            id = models.TextField(blank=True, null=True, unique=True)
            id2 = models.TextField(blank=True, null=True)
        
            class Meta:
                managed = False
                db_table = 'remote_test'


      2. db_column은 현재 필드명으로 선언된 id, name, age, num 과 같은 변수이름이 아닌 다른 필드를 사용하고 싶을 때 선언합니다. 위와같이 db_column = 'test'라고 정의하면 실제 쿼리에서는 num이라는 group테이블 내의 컬럼이 remote_test테이블의 id값을 참조하는 것이 아닌 선언해놓은 test라는 컬럼이 참조하도록 쿼리가 짜여집니다.
        1. 위 모델의 inner join 결과

          SELECT "group"."id", "group"."name", "group"."age", "group"."test", "remote_test"."num", "remote_test"."id", "remote_test"."id2" FROM "group" INNER JOIN "remote_test" ON ( "group"."test" = "remote_test"."id" ) ORDER BY "group"."id" ASC


      3. 만약 db_column만 정의를 했다면 1번과 같은 문제가 발생합니다.

    3. JOIN

    1) select_related()

    지금까지의 작업은 해당 함수를 사용하기 위해 지정해놓은 것입니다. 위처럼 모델의 정의가 전부 되어있다고 가정하면 join은 다음처럼 쉽습니다.

    Group.objects.select_related() 
     
    # 1. Group.objects.select_related('other_foreign_key') 

    select_related()의 join은 기본값으로 inner join이며 파라미터를 아무것도 안적을 시, foreign key로 비교를 합니다.

    만약 foriegn key가 여러개 잡혀있고 여기서 골라서 사용하고 싶을 때, 파라미터에 field명을 명시합니다. 

    foreign key로 잡혀있지 않은 field를 명시하면 다음과 같은 에러가 발생합니다.

    Invalid field name(s) given in select_related: 'other_foreign_key'. Choices are: num

    2) extra()

    2)과 3)은 모델에서 foreign key가 걸려있지 않거나 model에 테이블 자체가 정의되어 있지 않을 때 사용합니다.

     Group.objects.extra(tables=['remote_test'], where=['remote_test.id=group.num'])

    위와 같이 사용을 합니다. 해당 코드의 실제 쿼리는 다음과 같습니다.

    SELECT "group"."id", "group"."name", "group"."age", "group"."num" FROM "group" , "remote_test" WHERE (remote_test.id=group.num) ORDER BY "group"."id" ASC

    3) raw()

    raw()는 sql쿼리와 같이 생쿼리를 사용하면 되므로 사용법은 생략하겠습니다.

    extra()와 raw()의 차이점

    둘 간의 차이점은 먼저 return type입니다. extra()함수를 사용해서 나오는 결과타입은 queryset이고 raw()의 return type은 rawqueryset입니다. join을 한 다음 orm으로 다시 작업을 하기 위해선 queryset으로 반환이 되어야 합니다. rawqueryset 타입은 해당쿼리 이후 별 다른 작업을 하지 않은 end point일 경우에만 사용하는 것이 좋습니다.


    요약

    extra()는 queryset 타입으로 return되서 orm을 다시 사용할 수 있음

    rawqueryset()은 rawqueryset 타입으로 return되서 orm을 다시 사용할 수 없음

    참조

    foreign key 필드에 '_id'가 붙는 문제 해결 방법: http://stackoverflow.com/questions/8223519/preventing-django-from-appending-id-to-a-foreign-key-field

    db_column에 대한 참조문서 : https://docs.djangoproject.com/ja/1.10/ref/models/fields/#db-column

    to_field에 대한 참조문서 : https://docs.djangoproject.com/ja/1.10/ref/models/fields/#django.db.models.ForeignKey.to_field


    댓글