Quali sono i modi per evitare la duplicazione della logica tra classi di dominio e query SQL?

21

L'esempio qui sotto è totalmente artificiale e il suo unico scopo è quello di ottenere il mio punto di vista.

Supponiamo di avere una tabella SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Classe dominio:

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

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

Ora supponiamo di avere l'obbligo di mostrare all'utente l'area totale di tutti i rettangoli nel database. Posso farlo recuperando tutte le righe del tavolo, trasformandole in oggetti e iterando su di esse. Ma questo sembra semplicemente stupido, perché ho un sacco di rettangoli nel mio tavolo.

Quindi faccio questo:

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

Questo è facile, veloce e utilizza i punti di forza del database. Tuttavia, introduce la logica duplicata, perché ho lo stesso calcolo anche nella mia classe di dominio.

Naturalmente, per questo esempio la duplicazione della logica non è affatto fatale. Tuttavia, affronterò lo stesso problema con le mie altre classi di dominio, che sono più complesse.

    
posta Escape Velocity 21.07.2015 - 21:58
fonte

4 risposte

11

Come sottolineato da lxrec, esso varierà da codebase a codebase. Alcune applicazioni ti permetteranno di inserire questo tipo di logica aziendale in funzioni e / o query SQL e ti permetteranno di eseguirle ogni volta che avrai bisogno di mostrare quei valori all'utente.

A volte può sembrare stupido, ma è meglio codificare per correttezza la performance come obiettivo primario.

Nel tuo esempio, se stai mostrando il valore dell'area per un utente in un modulo web, dovresti:

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.

È stupido per cose semplici come quella sul campione, ma potrebbe essere necessario per cose più complesse come il calcolo dell'IRR di un investimento di un cliente in un sistema bancario.

Codice per la correttezza . Se il tuo software è corretto, ma lento, avrai la possibilità di ottimizzare dove ti serve (dopo la profilazione). Se ciò significa mantenere una parte della logica aziendale nel database, così sia. Ecco perché abbiamo tecniche di refactoring.

Se diventa lento, o non risponde, potresti avere alcune ottimizzazioni da fare, come violare il principio DRY, che non è un peccato se ti circondi del test dell'unità e della congruenza.

    
risposta data 21.07.2015 - 22:26
fonte
2

Dici che l'esempio è artificiale, quindi non so se quello che sto dicendo qui si adatta alla tua situazione attuale, ma la mia risposta è - usa un ORM (Object-relational mapping) per definire la struttura e l'interrogazione / manipolazione del tuo database. In questo modo non hai una logica duplicata, dal momento che tutto sarà definito nei modelli.

Ad esempio, usando il framework Django (python), definirai la tua classe di dominio rettangolo come la seguente model :

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

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

Per calcolare l'area totale (senza filtri) definisci:

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

Come altri hanno già accennato, dovresti prima scrivere il codice per la correttezza e ottimizzare solo quando hai veramente colto il collo di bottiglia. Quindi, se in un secondo momento decidi, devi assolutamente ottimizzare, puoi passare alla definizione di una query non elaborata, ad esempio:

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')
    
risposta data 22.07.2015 - 21:30
fonte
1

Ho scritto un esempio stupido per spiegare un'idea:

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

Quindi, se hai qualche logica:

var logic = "MULTIPLY:0,1";

Puoi riutilizzarlo nelle classi di dominio:

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

O nel tuo livello di generazione SQL:

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

E, naturalmente, puoi cambiarlo facilmente. Prova questo:

logic = "MULTIPLY:0,1,1,1";
    
risposta data 22.07.2015 - 00:01
fonte
-1

Come ha detto @Machado, il modo più semplice per farlo è evitarlo e fare tutta l'elaborazione nel tuo java principale. Tuttavia, è ancora possibile dover codificare la base con codice simile senza ripetersi generando il codice per entrambi i codici base.

Ad esempio, se utilizzi cog attiva la generazione dei tre snippet da una definizione comune

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

da un file di riferimento

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)
    
risposta data 01.08.2015 - 16:39
fonte

Leggi altre domande sui tag