Jakie są sposoby uniknięcia powielania logiki między klasami domen a zapytaniami SQL?

21

Poniższy przykład jest całkowicie sztuczny, a jego jedynym celem jest przekazanie mojego punktu widzenia.

Załóżmy, że mam tabelę SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Klasa domeny:

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

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

Załóżmy teraz, że mam wymaganie, aby pokazać użytkownikowi całkowity obszar wszystkich prostokątów w bazie danych. Mogę to zrobić, pobierając wszystkie rzędy stołu, obracając je w obiekty i iterując po nich. Ale to wygląda po prostu głupio, ponieważ mam wiele prostokątów w swoim stole.

Więc robię to:

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

Jest to łatwe, szybkie i wykorzystuje zalety bazy danych. Wprowadza jednak zduplikowaną logikę, ponieważ mam również takie same obliczenia w mojej klasie domeny.

Oczywiście w tym przykładzie powielanie logiki wcale nie jest śmiertelne. Jednak mam ten sam problem z innymi klasami domen, które są bardziej złożone.

Prędkość ucieczki
źródło
1
Podejrzewam, że optymalne rozwiązanie będzie się bardzo różnić między bazami kodów, więc czy mógłbyś krótko opisać jeden z bardziej złożonych przykładów, który sprawia ci problemy?
Ixrec
2
@lxrec: Raporty. Aplikacja biznesowa, która ma reguły, które wychwytuję na zajęciach, a także muszę tworzyć raporty zawierające te same informacje, ale skondensowane. Obliczenia VAT, płatności, zarobki, tego rodzaju rzeczy.
Escape Velocity
1
Czy nie jest to również kwestia dystrybucji obciążenia między serwerem a klientami? Jasne, po prostu zrzucenie buforowanego wyniku obliczeń do klienta jest najlepszym wyborem, ale jeśli dane często się zmieniają i jest wiele żądań, może być korzystne, aby móc rzucić składniki i przepis na klienta zamiast gotowanie dla nich posiłku. Myślę, że niekoniecznie złe jest posiadanie więcej niż jednego węzła w systemie rozproszonym, który może zapewnić określoną funkcjonalność.
null
Myślę, że najlepszym sposobem jest wygenerowanie takich kodów. Wyjaśnię później.
Xavier Combelle

Odpowiedzi:

11

Jak wskazał lxrec, będzie się różnić w zależności od bazy kodu. Niektóre aplikacje umożliwiają umieszczenie tego rodzaju logiki biznesowej w funkcjach SQL i / lub zapytaniach oraz pozwalają na ich uruchomienie w dowolnym momencie, w którym użytkownik chce pokazać te wartości.

Czasami może się to wydawać głupie, ale lepiej jest zakodować poprawność niż wydajność jako główny cel.

W przykładzie, jeśli wyświetlasz wartość obszaru dla użytkownika w formularzu internetowym, musisz:

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.

To głupie dla prostych rzeczy, takich jak ta z próbki, ale może być konieczne bardziej skomplikowane rzeczy, takie jak obliczanie IRR inwestycji klienta w system bankowy.

Kod poprawności . Jeśli twoje oprogramowanie jest poprawne, ale powolne, będziesz mieć szansę na optymalizację tam, gdzie potrzebujesz (po profilowaniu). Jeśli to oznacza zachowanie logiki biznesowej w bazie danych, niech tak będzie. Dlatego mamy techniki refaktoryzacji.

Jeśli stanie się powolny lub przestanie odpowiadać, być może będziesz musiał wykonać pewne optymalizacje, takie jak naruszenie zasady OSUSZANIA, co nie jest grzechem, jeśli otoczysz się właściwym testowaniem jednostkowym i testowaniem spójności.

Machado
źródło
1
Problem z umieszczeniem (proceduralnej) logiki biznesowej w SQL polega na tym, że refaktoryzacja jest wyjątkowo bolesna. Nawet jeśli masz najwyższej klasy narzędzia refaktoryzujące SQL, zwykle nie współpracują one z narzędziami refaktoryzującymi kod w twoim IDE (lub przynajmniej nie widziałem takiego zestawu narzędzi)
Roland Tepp
2

Mówisz, że przykład jest sztuczny, więc nie wiem, czy to, co mówię, pasuje do twojej aktualnej sytuacji, ale moja odpowiedź brzmi - użyj warstwy ORM (Object-relational mapping) do zdefiniowania struktury i zapytania / manipulacji twoja baza danych. W ten sposób nie masz zduplikowanej logiki, ponieważ wszystko zostanie zdefiniowane w modelach.

Na przykład, używając frameworka Django (python), zdefiniowałbyś klasę domeny prostokąta jako następujący model :

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

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

Aby obliczyć całkowity obszar (bez filtrowania), należy zdefiniować:

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

Jak wspomnieli inni, powinieneś najpierw kodować poprawność, a optymalizować tylko wtedy, gdy naprawdę napotkasz wąskie gardło. Więc jeśli później zdecydujesz, absolutnie musisz zoptymalizować, możesz przejść do definiowania surowego zapytania, takiego jak:

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')
yoniLavi
źródło
1

Napisałem głupi przykład, aby wyjaśnić pewien pomysł:

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

Jeśli masz logikę:

var logic = "MULTIPLY:0,1";

Możesz go ponownie użyć w klasach domen:

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

Lub w warstwie generacji sql:

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

I oczywiście możesz to łatwo zmienić. Spróbuj tego:

logic = "MULTIPLY:0,1,1,1";
astef
źródło
-1

Jak powiedział @Machado, najprostszym sposobem na to jest uniknięcie tego i wykonanie całego przetwarzania w głównej Javie. Jednak nadal możliwe jest kodowanie kodu bazowego za pomocą podobnego kodu bez powtarzania się przez wygenerowanie kodu dla obu baz kodu.

Na przykład za pomocą zębatki enable można wygenerować trzy fragmenty ze wspólnej definicji

fragment 1:

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

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

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

z jednego pliku referencyjnego

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
źródło