Skip to content
Viacheslav Naydenov edited this page Jul 17, 2014 · 1 revision

Отображение связи один-ко-многим (tut2.cpp)

Рассмотрим связь один-ко-многим на примере отношения Клиент — Заказы. У одного клиента может быть ноль или более заказов в некоторой учетной системе, при этом каждый заказ относится к какому-либо клиенту. Клиенты хранятся в таблице client_tbl, их заказы — в таблице order_tbl.

На уровне SQL эту связь можно выразить так: на колонке client_id в т. н. дочерней таблице order_tbl присутствует ограничение внешнего ключа, ссылающееся на колонку id в т. н. родительской таблице client_tbl.

На уровне ORM такая связь представляется свойствами объектов. Экземпляр класса, соответствующего дочерней таблице, обычно имеет свойство-объектную ссылку, которая ссылается на родительский объект. С другой стороны такого отношения, экземпляр класса, соответствующий родительской таблице, может иметь свойство-коллекцию объектов (которая иногда называется "backref" - обратная ссылка), чтобы иметь возможность просмотреть все свои дочерние объекты.

Вначале определим схему данных, содержащую два класса Client и Order, отображенные в две таблицы client_tbl и order_tbl. Отдельно опишем связь между ними.

<schema>
    <table name="client_tbl" sequence="client_seq" class="Client" xml-name="client">
        <column name="id" type="longint">
            <primary-key />
        </column>
        <column name="dt" type="datetime" null="false" default="sysdate" />
        <column name="name" type="string" size="100" null="false" />
        <column name="email" type="string" size="100" null="false" />
        <column name="phone" type="string" size="50" null="true" />
        <column name="budget" type="decimal" />
    </table>
    <table name="order_tbl" sequence="order_seq" class="Order" xml-name="order">
        <column name="id" type="longint">
            <primary-key />
        </column>
        <column name="client_id" type="longint" null="false">
            <foreign-key table="client_tbl" key="id"/>
        </column>
        <column name="dt" type="datetime" null="false" default="sysdate" />
        <column name="memo" type="string" size="100" />
        <column name="total_sum" type="decimal" null="false" />
        <column name="paid_sum" type="decimal" default="0" />
        <column name="paid_dt" type="datetime" />
    </table>
    <relation type="one-to-many">
        <one class="Client" property="orders" />
        <many class="Order" property="owner" />
    </relation>
</schema>

В описании таблицы order_tbl указано, что колонка client_id, является внешним ключом, она играет роль ссылки. Когда между двумя таблицами существует ровно одна подобная связь, то при описании связи с помощью элемента <relation> нет необходимости в дополнительных указаниях.

Код SQLite для приведенной выше схемы (использование утилиты yborm_gen см. в Tutorial1) будет выглядеть так:

CREATE TABLE client_tbl (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL,
    phone VARCHAR(50),
    budget NUMERIC
);

CREATE TABLE order_tbl (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    client_id INTEGER NOT NULL,
    dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    memo VARCHAR(100),
    total_sum NUMERIC NOT NULL,
    paid_sum NUMERIC DEFAULT 0,
    paid_dt TIMESTAMP
    , FOREIGN KEY (client_id) REFERENCES client_tbl(id)
);

Следующая программа использует доменные классы из приведенной выше схемы (использование утилиты yborm_gen см. в Tutorial1). Эта программа сначала создаёт дочерний объект, присваивает какие-то значения его полям, затем она создаёт родительский объект, потом она связывает дочерний объект с родительским. В конце она сохраняет объекты в сессии и записывает их в базу, завершая транзакцию (session.commit();).

#include <iostream>
#include "domain/Client.h"
#include "domain/Order.h"
int main()
{
    std::auto_ptr<Yb::SqlConnection> conn(new Yb::SqlConnection(
            "sqlite+sqlite://./tut2.sqlite"));
    Yb::Engine engine(Yb::Engine::READ_WRITE, conn);
    Yb::Session session(Yb::init_schema(), &engine);

    Domain::Order::Holder order;
    std::string amount;
    std::cout << "Enter order amount: \n";
    std::cin >> amount;
    order->total_sum = Yb::Decimal(amount);

    Domain::Client::Holder client;
    std::string name, email;
    std::cout << "Enter name, email: \n";
    std::cin >> name >> email;
    client->name = name;
    client->email = email;
    client->dt = Yb::now();

    std::cout << "Client's orders count: " << client->orders.size() << "\n";
    order->owner = client;
    std::cout << "Client's orders count: " << client->orders.size() << "\n";

    order->save(session);
    client->save(session);
    session.commit();
    std::cout << order->xmlize(1)->serialize() << std::endl;
    return 0;
}

В этом примере видно, что связывание дочернего (класс Order) объекта с родительским (класс Client) выглядит как присваивание (order->owner = client;). Однако, этот пример оперирует с объектами доменных классов не напрямую, как в Tutorial1, вместо этого используются классы <DomainClass>::Holder, которые необходимы для реализации вложенных и рекурсивных объектных ссылок. То есть, все свойства-объектные ссылки реализованы с использованием классов <DomainClass>::Holder, а для их разыменования нужно использовать стрелочку (->), а не точку (.).

Компилируем пример:

$ c++ -I. -I$YBORM_ROOT/include/yb -o tut2 tut2.cpp domain/Client.cpp domain/Order.cpp -L$YBORM_ROOT/lib -lybutil -lyborm

Запускаем на выполнение:

$ ./tut2
Enter order amount: 
45.67
Enter name, email: 
Petya [email protected]
Client's orders count: 0
Client's orders count: 1
<order><id>2</id><owner><id>2</id><dt>2014-06-01T17:25:32</dt><name>Petya</name><email>[email protected]</email><budget is_null="1"/></owner><dt>2014-06-01T17:25:17</dt><memo is_null="1"/><total-sum>45.67</total-sum><paid-sum>0</paid-sum><paid-dt is_null="1"/></order>

Если проследить операторы SQL, включив журнал сообщений по примеру Tutorial1, то увидим следующие подробности:

14-06-01 17:25:32.293 31927/31927 DEBG orm: flush started
14-06-01 17:25:32.293 31927/31927 DEBG sql: begin transaction
14-06-01 17:25:32.293 31927/31927 DEBG sql: prepare: INSERT INTO client_tbl (dt, name, email, budget) VALUES (?, ?, ?, ?)
14-06-01 17:25:32.294 31927/31927 DEBG sql: bind: (DateTime, String, String, Decimal)
14-06-01 17:25:32.294 31927/31927 DEBG sql: exec prepared: p1="'2014-06-01 17:25:32'" p2="'Petya'" p3="'[email protected]'" p4="NULL"
14-06-01 17:25:32.295 31927/31927 DEBG sql: prepare: SELECT SEQ LID FROM SQLITE_SEQUENCE WHERE NAME = 'client_tbl'
14-06-01 17:25:32.295 31927/31927 DEBG sql: exec prepared:
14-06-01 17:25:32.295 31927/31927 DEBG sql: fetch: LID='2' 
14-06-01 17:25:32.295 31927/31927 DEBG sql: fetch: no more rows
14-06-01 17:25:32.295 31927/31927 DEBG sql: prepare: INSERT INTO order_tbl (client_id, dt, memo, total_sum, paid_sum, paid_dt) VALUES (?, ?, ?, ?, ?, ?)
14-06-01 17:25:32.295 31927/31927 DEBG sql: bind: (LongInt, DateTime, String, Decimal, Decimal, DateTime)
14-06-01 17:25:32.295 31927/31927 DEBG sql: exec prepared: p1="2" p2="'2014-06-01 17:25:17'" p3="NULL" p4="45.67" p5="0" p6="NULL"
14-06-01 17:25:32.295 31927/31927 DEBG sql: prepare: SELECT SEQ LID FROM SQLITE_SEQUENCE WHERE NAME = 'order_tbl'
14-06-01 17:25:32.296 31927/31927 DEBG sql: exec prepared:
14-06-01 17:25:32.296 31927/31927 DEBG sql: fetch: LID='2' 
14-06-01 17:25:32.296 31927/31927 DEBG sql: fetch: no more rows
14-06-01 17:25:32.296 31927/31927 DEBG orm: flush finished OK
14-06-01 17:25:32.296 31927/31927 DEBG sql: commit
14-06-01 17:25:32.395 31927/31927 DEBG sql: prepare: SELECT order_tbl.id, order_tbl.client_id, order_tbl.dt, order_tbl.memo, order_tbl.total_sum, order_tbl.paid_sum, order_tbl.paid_dt FROM order_tbl WHERE order_tbl.id = ?
14-06-01 17:25:32.395 31927/31927 DEBG sql: exec prepared: p1="2"
14-06-01 17:25:32.395 31927/31927 DEBG sql: fetch: ID='2' CLIENT_ID='2' DT='2014-06-01T17:25:17' MEMO=NULL TOTAL_SUM='45.67' RECEIPT_SUM='0' RECEIPT_DT=NULL 
14-06-01 17:25:32.395 31927/31927 DEBG sql: fetch: no more rows
14-06-01 17:25:32.395 31927/31927 DEBG sql: prepare: SELECT client_tbl.id, client_tbl.dt, client_tbl.name, client_tbl.email, client_tbl.budget FROM client_tbl WHERE client_tbl.id = ?
14-06-01 17:25:32.396 31927/31927 DEBG sql: exec prepared: p1="2"
14-06-01 17:25:32.396 31927/31927 DEBG sql: fetch: ID='2' DT='2014-06-01T17:25:32' NAME='Petya' EMAIL='[email protected]' BUDGET=NULL 
14-06-01 17:25:32.396 31927/31927 DEBG sql: fetch: no more rows

Обратите внимание, что объекты вставлены в корректной последовательности (первым — родительский, вторым — дочерний), так как используется топологическая сортировка графа зависимых объектов. Значение внешнего ключа будет присвоено автоматически, так же как и значения первичных ключей.

При связывании объектов с помощью присваивания происходит и изменение коллекции orders в соответствующем экземпляре класса Client. Дело в том, что оба свойства client->orders и order->owner используют один и тот же внутренний объект RelationObject. Можно добиться такого же эффекта вставкой в свойство-коллекцию. Заменяем присваивание со стороны подчиненного объекта

    order->owner = client;

на вставку в коллекцию со стороны главного объекта

    client->orders.insert(*order);

Можно увидеть, что оба эти варианта абсолютно равносильны.

Clone this wiki locally