Auf welche Weise können Sie doppelte Logik zwischen Domänenklassen und SQL-Abfragen vermeiden?

21

Das folgende Beispiel ist völlig künstlich und hat nur den Zweck, meinen Standpunkt zu verdeutlichen.

Angenommen, ich habe eine SQL-Tabelle:

CREATE TABLE rectangles (
  width int,
  height int 
);

Domain-Klasse:

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

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

Angenommen, ich möchte dem Benutzer die Gesamtfläche aller Rechtecke in der Datenbank anzeigen. Ich kann das tun, indem ich alle Zeilen der Tabelle abrufe, sie in Objekte verwandle und über sie iteriere. Aber das sieht einfach blöd aus, weil ich sehr viele Rechtecke in meinem Tisch habe.

Also mache ich das:

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

Dies ist einfach, schnell und nutzt die Stärken der Datenbank. Es wird jedoch eine doppelte Logik eingeführt, da ich die gleiche Berechnung auch in meiner Domänenklasse habe.

In diesem Beispiel ist die Vervielfältigung der Logik natürlich überhaupt nicht fatal. Ich habe jedoch das gleiche Problem mit meinen anderen Domänenklassen, die komplexer sind.

Entkomme der Geschwindigkeit
quelle
1
Ich vermute, dass die optimale Lösung von Codebasis zu Codebasis sehr unterschiedlich ist. Können Sie also kurz eines der komplexeren Beispiele beschreiben, die Ihnen Probleme bereiten?
Ixrec
2
@lxrec: Berichte. Eine Geschäftsanwendung mit Regeln, die ich in Klassen aufzeichne, und ich muss auch Berichte erstellen, die dieselben Informationen enthalten, aber komprimiert sind. MwSt-Berechnungen, Zahlungen, Einnahmen, solche Sachen.
Escape Velocity
1
Geht es nicht auch darum, die Last zwischen Server und Clients zu verteilen? Sicher, nur das zwischengespeicherte Ergebnis der Berechnung an einen Kunden zu senden, ist die beste Wahl. Wenn sich die Daten jedoch häufig ändern und es viele Anfragen gibt, kann es vorteilhaft sein, die Zutaten und das Rezept einfach auf den Kunden zu werfen, anstatt das Essen für sie kochen. Ich denke, es ist nicht unbedingt eine schlechte Sache, mehr als einen Knoten in einem verteilten System zu haben, der eine bestimmte Funktionalität bereitstellen kann.
null
Ich denke, der beste Weg ist, solche Codes zu generieren. Ich werde es später erklären.
Xavier Combelle

Antworten:

11

Wie lxrec hervorhob, wird es von Codebasis zu Codebasis variieren. In einigen Anwendungen können Sie diese Art von Geschäftslogik in SQL-Funktionen und / oder Abfragen einfügen und sie ausführen, wann immer Sie diese Werte dem Benutzer anzeigen müssen.

Manchmal mag es dumm erscheinen, aber es ist besser, auf Korrektheit zu programmieren als Leistung als primäres Ziel.

Wenn Sie in Ihrem Beispiel den Wert des Bereichs für einen Benutzer in einem Webformular anzeigen, müssen Sie:

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.

Es ist dumm für einfache Dinge wie die in der Stichprobe, aber es kann notwendig sein, komplexere Dinge wie die Berechnung des IRR einer Investition eines Kunden in ein Bankensystem.

Code für die Richtigkeit . Wenn Ihre Software korrekt, aber langsam ist, haben Sie die Möglichkeit, zu optimieren, wo Sie es benötigen (nach der Profilerstellung). Wenn dies bedeutet, dass ein Teil der Geschäftslogik in der Datenbank verbleibt, ist es auch so. Deshalb haben wir Refactoring-Techniken.

Wenn es langsam wird oder nicht mehr reagiert, müssen Sie möglicherweise einige Optimierungen vornehmen, z. B. das DRY-Prinzip zu verletzen. Dies ist keine Sünde, wenn Sie sich auf die ordnungsgemäße Prüfung der Einheit und der Konsistenz beschränken.

Machado
quelle
1
Das Problem beim Einfügen von (prozeduraler) Geschäftslogik in SQL ist, dass die Umgestaltung äußerst schmerzhaft ist. Selbst wenn Sie erstklassige SQL-Refactoring-Tools haben, lassen sich diese normalerweise nicht mit Code-Refactoring-Tools in Ihrer IDE verbinden (oder zumindest habe ich ein solches Toolset noch nicht gesehen)
Roland Tepp
2

Sie sagen, dass das Beispiel künstlich ist, daher weiß ich nicht, ob das, was ich hier sage, zu Ihrer tatsächlichen Situation passt, aber meine Antwort lautet: Verwenden Sie eine ORM -Ebene (Object-Relational Mapping), um die Struktur und Abfrage / Manipulation von zu definieren Ihre Datenbank. Auf diese Weise haben Sie keine doppelte Logik, da alles in den Modellen definiert wird.

Wenn Sie beispielsweise das Django- Framework (Python) verwenden, definieren Sie Ihre Rechteckdomänenklasse als das folgende Modell :

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

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

So berechnen Sie die Gesamtfläche (ohne Filterung):

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

Wie bereits erwähnt, sollten Sie zunächst auf Korrektheit codieren und erst optimieren, wenn Sie wirklich einen Engpass feststellen. Wenn Sie also zu einem späteren Zeitpunkt entscheiden, dass Sie unbedingt optimieren müssen, können Sie zur Definition einer unformatierten Abfrage übergehen, z.

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

Ich habe ein albernes Beispiel geschrieben, um eine Idee zu erklären:

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";
    }
}

Also, wenn Sie eine Logik haben:

var logic = "MULTIPLY:0,1";

Sie können es in Domänenklassen wiederverwenden:

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

Oder in Ihrer SQL-Generierungsschicht:

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

Und natürlich können Sie es leicht ändern. Versuche dies:

logic = "MULTIPLY:0,1,1,1";
astef
quelle
-1

Wie @Machado sagte, ist der einfachste Weg, dies zu vermeiden und die gesamte Verarbeitung in Ihrem Haupt-Java durchzuführen. Es ist jedoch weiterhin möglich, die Codebasis mit ähnlichem Code zu codieren, ohne dass Sie sich selbst wiederholen müssen, indem Sie den Code für beide Codebasis generieren.

Verwenden Sie beispielsweise cog enable, um die drei Ausschnitte aus einer gemeinsamen Definition zu generieren

Snippet 1:

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

Snippet 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]]]*/
}

Snippet 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]]]*/

aus einer Referenzdatei

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)
Xavier Combelle
quelle