도메인 클래스와 SQL 쿼리 간의 논리 중복을 피하는 방법은 무엇입니까?


21

아래의 예는 완전히 인공적이며 유일한 목적은 내 요점을 파악하는 것입니다.

SQL 테이블이 있다고 가정하십시오.

CREATE TABLE rectangles (
  width int,
  height int 
);

도메인 클래스 :

public class Rectangle {
  private int width;
  private int height;

  /* My business logic */
  public int area() {
    return width * height;
  }
}

이제 데이터베이스에있는 모든 사각형의 총 면적을 사용자에게 표시해야한다고 가정합니다. 테이블의 모든 행을 가져 와서 객체로 바꾸고 반복하여 수행 할 수 있습니다. 그러나 이것은 테이블에 많은 사각형이 있기 때문에 어리석은 것처럼 보입니다.

그래서 나는 이것을한다 :

SELECT sum(r.width * r.height)
FROM rectangles r

이것은 쉽고 빠르며 데이터베이스의 장점을 사용합니다. 그러나 도메인 클래스에서 동일한 계산을 수행하기 때문에 중복 된 논리가 도입됩니다.

물론,이 예에서 논리의 복제는 전혀 치명적이지 않습니다. 그러나 다른 도메인 클래스와 같은 문제가 더 복잡합니다.


1
최적의 솔루션이 코드베이스마다 다르기 때문에 문제가 발생하는 더 복잡한 예제 중 하나를 간단히 설명 할 수 있습니까?
Ixrec

2
@lxrec : 보고서. 클래스에서 캡처하는 규칙이 있고 동일한 정보를 표시하지만 요약 된 보고서를 작성해야하는 비즈니스 애플리케이션입니다. 부가가치세 계산, 지불, 수입, 그런 것들.
탈출 속도

1
서버와 클라이언트간에로드를 분산시키는 문제가 아닌가? 물론, 캐시 된 계산 결과를 클라이언트에 덤프하는 것이 최선의 방법이지만 데이터가 자주 변경되고 요청이 많으면 클라이언트 대신 재료와 레시피를 던질 수있는 것이 유리할 수 있습니다 그들을 위해 식사를 요리. 분산 시스템에 특정 기능을 제공 할 수있는 둘 이상의 노드를 갖는 것이 반드시 나쁜 것은 아니라고 생각합니다.
null

가장 좋은 방법은 그러한 코드를 생성하는 것입니다. 나중에 설명하겠습니다.
Xavier Combelle

답변:


11

lxrec가 지적했듯이 코드베이스마다 다릅니다. 일부 응용 프로그램에서는 이러한 종류의 비즈니스 논리를 SQL 함수 및 / 또는 쿼리에 넣고 사용자에게 해당 값을 표시해야 할 때마다 실행할 수 있습니다.

때로는 어리석은 것처럼 보일 수 있지만 성능을 기본 목표로하는 것보다 정확성을 코드화하는 것이 좋습니다.

샘플에서 웹 양식에 사용자의 영역 값을 표시하려면 다음을 수행해야합니다.

1) Do a post/get to the server with the values of x and y;
2) The server would have to create a query to the DB Server to run the calculations;
3) The DB server would make the calculations and return;
4) The webserver would return the POST or GET to the user;
5) Final result shown.

샘플에있는 것과 같은 간단한 것은 어리석은 일이지만 뱅킹 시스템에서 고객의 투자에 대한 IRR을 계산하는 것과 같이 더 복잡한 일이 필요할 수 있습니다.

정확성을위한 코드 . 소프트웨어가 정확하지만 속도가 느리면 프로파일 링 후 필요한 위치를 최적화 할 수 있습니다. 이것이 데이터베이스에 일부 비즈니스 로직을 유지하는 것을 의미하는 경우에도 마찬가지입니다. 우리가 리팩토링 기술을 가지고있는 이유입니다.

속도가 느리거나 응답이 없으면 DRY 원칙을 위반하는 것과 같이 최적화가 필요할 수 있습니다. 이는 적절한 단위 테스트 및 일관성 테스트를 둘러싸고 있다면 죄가 아닙니다.


1
(프로 시저) 비즈니스 로직을 SQL에 넣는 데 따르는 문제는 리팩토링하기가 매우 어렵다는 것입니다. 최고 수준의 SQL 리팩토링 도구를 사용하더라도 IDE에서 코드 리팩토링 도구와 인터페이스하지 않습니다 (또는 적어도 아직 그러한 도구 세트를 보지 못했습니다)
Roland Tepp

2

당신은 예제가 인공적인 것이므로 여기에서 말하는 것이 실제 상황에 맞는지 모르겠지만 대답은 ORM (Object-Relational Mapping) 레이어를 사용하여 구조와 쿼리 / 조작을 정의합니다. 데이터베이스. 이렇게하면 모델에 모든 것이 정의되므로 중복 된 로직이 없습니다.

예를 들어, Django (python) 프레임 워크를 사용하여 사각형 도메인 클래스를 다음 모델 로 정의합니다 .

class Rectangle(models.Model):
    width = models.IntegerField()
    height = models.IntegerField()

    def area(self):
        return self.width * self.height

필터링하지 않고 총 면적을 계산하려면 다음을 정의하십시오.

def total_area():
    return sum(rect.area() for rect in Rectangle.objects.all())

다른 사람들이 언급했듯이, 먼저 올바른 코드를 작성하고 병목 현상이 발생할 때만 최적화해야합니다. 따라서 나중에 결정하는 경우 절대적으로 최적화해야하는 경우 다음과 같은 원시 쿼리 정의로 전환 할 수 있습니다.

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')

1

아이디어를 설명하기 위해 바보 같은 예를 작성했습니다.

class BinaryIntegerOperation
{
    public int Execute(string operation, int operand1, int operand2)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            var args = split[1].Split(',');
            var result = IsFirstOperand(args[0]) ? operand1 : operand2;
            for (var i = 1; i < args.Length; i++)
            {
                result *= IsFirstOperand(args[i]) ? operand1 : operand2;
            }
            return result;
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    public string ToSqlExpression(string operation, string operand1Name, string operand2Name)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            return string.Join("*", split[1].Split(',').Select(a => IsFirstOperand(a) ? operand1Name : operand2Name));
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    private bool IsFirstOperand(string code)
    {
        return code == "0";
    }
}

따라서 논리가 있다면 :

var logic = "MULTIPLY:0,1";

도메인 클래스에서 재사용 할 수 있습니다.

var op = new BinaryIntegerOperation();
Console.WriteLine(op.Execute(logic, 3, 6));

또는 SQL 생성 계층에서 :

Console.WriteLine(op.ToSqlExpression(logic, "r.width", "r.height"));

물론 쉽게 변경할 수 있습니다. 이 시도:

logic = "MULTIPLY:0,1,1,1";

-1

@Machado가 말했듯이, 가장 쉬운 방법은 그것을 피하고 기본 Java에서 모든 처리를 수행하는 것입니다. 그러나 두 코드베이스에 대한 코드를 생성하여 자체 반복하지 않고 유사한 코드로 코드베이스를 작성해야 할 수도 있습니다.

예를 들어 cog enable을 사용하여 공통 정의에서 세 개의 스 니펫을 생성

스 니펫 1 :

/*[[[cog
from generate import generate_sql_table
cog.outl(generate_sql_table("rectangle"))
]]]*/
CREATE TABLE rectangles (
    width int,
    height int
);
/*[[[end]]]*/

스 니펫 2 :

public class Rectangle {
    /*[[[cog
      from generate import generate_domain_attributes,generate_domain_logic
      cog.outl(generate_domain_attributes("rectangle"))
      cog.outl(generate_domain_logic("rectangle"))
      ]]]*/
    private int width;
    private int height;
    public int area {
        return width * heigh;
    }
    /*[[[end]]]*/
}

스 니펫 3 :

/*[[[cog
from generate import generate_sql
cog.outl(generate_sql("rectangle","""
                       SELECT sum({area})
                       FROM rectangles r"""))
]]]*/
SELECT sum((r.width * r.heigh))
FROM rectangles r
/*[[[end]]]*/

하나의 참조 파일에서

import textwrap
import pprint

# the common definition 

types = {"rectangle":
    {"sql_table_name": "rectangles",
     "sql_alias": "r",
     "attributes": [
         ["width", "int"],
         ["height", "int"],
     ],
    "methods": [
        ["area","int","this.width * this.heigh"],
    ]
    }
 }

# the utilities functions

def generate_sql_table(name):
    type = types[name]
    attributes =",\n    ".join("{attr_name} {attr_type}".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])
    return """
CREATE TABLE {table_name} (
    {attributes}
);""".format(
    table_name=type["sql_table_name"],
    attributes = attributes
).lstrip("\n")


def generate_method(method_def):
    name,type,value =method_def
    value = value.replace("this.","")
    return textwrap.dedent("""
    public %(type)s %(name)s {
        return %(value)s;
    }""".lstrip("\n"))% {"name":name,"type":type,"value":value}


def generate_sql_method(type,method_def):
    name,_,value =method_def
    value = value.replace("this.",type["sql_alias"]+".")
    return name,"""(%(value)s)"""% {"value":value}

def generate_domain_logic(name):
    type = types[name]
    attributes ="\n".join(generate_method(method_def)
                   for method_def
                   in type["methods"])

    return attributes


def generate_domain_attributes(name):
    type = types[name]
    attributes ="\n".join("private {attr_type} {attr_name};".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])

    return attributes

def generate_sql(name,sql):
    type = types[name]
    fields ={name:value
             for name,value in
             (generate_sql_method(type,method_def)
              for method_def in type["methods"])}
    sql=textwrap.dedent(sql.lstrip("\n"))
    print (sql)
    return sql.format(**fields)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.