ドメインクラスと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
最適なソリューションはコードベースごとに大きく異なると思うので、トラブルを引き起こしているより複雑な例の1つを簡単に説明してもらえますか?
Ixrec

2
@lxrec:レポート。クラスでキャプチャしているルールを持つビジネスアプリケーション。また、同じ情報を表示するが要約されたレポートを作成する必要があります。VATの計算、支払い、収益など。
エスケープ速度

1
これは、サーバーとクライアント間で負荷を分散する問題でもありませんか?確かに、キャッシュされた計算結果をクライアントにダンプするのが最善の策ですが、データが頻繁に変更され、多くのリクエストがある場合は、成分とレシピをクライアントに投げるだけで済むのが有利かもしれません彼らのために食事を調理します。特定の機能を提供できる分散システムに複数のノードがあることは必ずしも悪いことではないと思います。
nullの

そのようなコードを生成するのが最善の方法だと思います。後で説明します。
ザビエルコンベル

回答:


11

lxrecが指摘したように、コードベースごとに異なります。一部のアプリケーションでは、これらの種類のビジネスロジックをSQL関数やクエリに入れたり、それらの値をユーザーに表示する必要があるときにいつでも実行したりできます。

ばかげているように見えることもありますが、主な目的としてパフォーマンスよりも正確さをコーディングする方が適切です。

サンプルでは、​​Webフォームでユーザーの領域の値を表示する場合、次のことを行う必要があります。

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には、コードのリファクタリングツールとのインタフェースはありません(少なくとも私はまだ、このようなツールセットを見ていない)
ローランドTEPP

2

あなたは例が人工的であると言うので、ここで私が言っていることがあなたの実際の状況に合っているかどうかはわかりませんが、私の答えは-ORM(オブジェクトリレーショナルマッピング)レイヤーを使用して構造とクエリ/操作を定義することですデータベース。そうすれば、すべてがモデルで定義されるため、重複したロジックはありません。

たとえば、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を使用して、共通の定義から3つのスニペットを生成します

スニペット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]]]*/

1つの参照ファイルから

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.