Які способи уникнути дублювання логіки між класами домену та 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
Я підозрюю, що оптимальне рішення буде дуже дивно відрізнятися від кодової бази до кодової бази, тож чи можете ви коротко описати один із більш складних прикладів, що доставляє вам проблеми?
Іксрек

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 (об'єктно-реляційне відображення) для визначення структури та запиту / маніпулювання вашу базу даних. Таким чином у вас немає дублюваної логіки, оскільки все буде визначено в моделях.

Наприклад, використовуючи рамку 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, дозволяють генерувати три фрагменти із загального визначення

фрагмент 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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.