Jii: Active Record для Node.js с API от Yii 2

    Jii

    Вступление


    Привет всем хабровчанам, любителям Yii и Node.js.
    Это вторая статья про фреймворк Jii (GitHub), в предыдущей статье мы рассматривали Объекты доступа к данным и конструктор запросов (Query Builder).
    Как и обещал, в этой статье я расскажу про использовании Active Record.

    Active Record


    Active Record обеспечивает объектно-ориентированный интерфейс для доступа и манипуляции данными, хранящихся в базах данных. Класс Active Record связан с таблицей базы данных, экземпляр класса соответствует строке этой таблицы, а атрибуты представляют собой значения определенного столбца в этой строке. Вместо того чтобы писать SQL запросы в явном виде, Вы имеете доступ к атрибутам Active Record и методам, манипулирующие данными.

    Предположим, что у нас есть Active Record класс `app.models.Customer`, который связан с таблице `customer` и `name` это имя колонки в таблице `customer`. Вы можете написать следующий код для добавления новой строки в таблицу `customer`:

    var customer = new app.models.Customer();
    customer.name = 'Vladimir';
    customer.save();
    

    Это эквивалентно следующему коду, в котором Вы можете допустить больше ошибок при написании и где могут быть несовместимости при различных видах данных:

    db.createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', {
        ':name': 'Vladimir',
    }).execute();
    

    Объявление классов Active Record


    Для начала, объявите класс Active Record, унаследовав Jii.sql.ActiveRecord. Каждый Active Record класс связан с таблицей базы данных, поэтому в этом классе необходимо переопределить статический метод Jii.sql.ActiveRecord.tableName() для указания с какой таблицей связан класс.

    В следующем примере, мы объявляем класс с именем `app.models.Customer` для таблицы `customer`.

    var Jii = require('jii');
    
    /**
    * @class app.models.Customer
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Customer', /** @lends app.models.Customer.prototype */{
    
        __extends: Jii.sql.ActiveRecord,
    
        __static: /** @lends app.models.Customer */{
    
            tableName: function() {
                return 'customer';
            }
    
        }
    
    });
    

    Класс Active Record является моделью, поэтому обычно мы кладем Active Record классы в пространство имен `models`.

    Класс Jii.sql.ActiveRecord наследует Jii.base.Model, это значит, что он наследует *все* возможности моделей, такие как аттрибуты, правила валидации, серелиализация данных и т.д.

    Создание соединения с БД


    По-умолчанию, Active Record использует компонент приложения `db`, который содержит экземпляр Jii.sql.BaseConnection для чтения и изменения данных в БД. Как описано в предыдущей статье, в разделе «Объекты доступа к базе данных», вы можете сконфигурировать компонент приложения `db` следующим образом:

    return {
        components: {
            db: {
                className: 'Jii.sql.mysql.Connection',
                host: '127.0.0.1',
                database: 'testdb',
                username: 'demo',
                password: 'demo',
                charset: 'utf8',
            }
        }
    };
    

    Если вы хотите использовать другое соединение с базой данных, вы должны переопределить метод Jii.sql.ActiveRecord.getDb():

    /**
    * @class app.models.Customer
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Customer', /** @lends app.models.Customer.prototype */{
    
        __extends: Jii.sql.ActiveRecord,
    
        __static: /** @lends app.models.Customer */{
    
            STATUS_INACTIVE: 0,
            STATUS_ACTIVE: 1,
    
            // ...
    
            getDb: function() {
                // use the "db2" application component
                return Jii.app.db2;
            }
    
        }
    
    });
    

    Выборка данных



    После объявления класса Active Record, вы можете использовать его для запроса данных из соответствующей таблицы БД.
    Для этого необходимо выполнить три действия:

    1. Создать новый объект запроса путем вызова метода Jii.sql.ActiveRecord.find();
    2. Сгенерировать объект запроса, вызывая методы создания запросов;
    3. Вызвать один из методов запроса для получения данных в виде записей Active Record.

    Эти действия очень схожи с действиями при работе с конструктором запросов. Разница только в том, что для создания объекта запроса необходимо вызвать метод Jii.sql.ActiveRecord.find(), а не создавать экземпляр через `new`.

    Рассмотрим несколько примеров, показывающие, как использовать Active Query для получения данных:

    // Возвращает клиента с ID 123
    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer.find()
        .where({id: 123})
        .one()
        .then(function(customer) {
            // ...
        });
    
    // Возвращает всех активных клиентов, отсортированных по ID
    // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
    app.models.Customer.find()
        .where({status: app.models.Customer.STATUS_ACTIVE})
        .orderBy('id')
        .all()
        .then(function(customers) {
            // ...
        });
    
    // Возвращает количество активных клиентов
    // SELECT COUNT(*) FROM `customer` WHERE `status` = 1
        app.models.Customer.find()
        .where({status': app.models.Customer.STATUS_ACTIVE})
        .count()
        .then(function(count) {
            // ...
        });
    
    // Возвращает всех клиентов в виде объекта, где ключами являются ID клиентов
    // SELECT * FROM `customer`
        app.models.Customer.find()
        .indexBy('id')
        .all()
        .then(function(customers) {
            // ...
        });
    

    Для упрощения получения моделей по ID созданы методы:
    • Jii.sql.ActiveRecord.findOne(): Возвращает экземпляр Active Record, соответствующий первой строке результата апроса.
    • Jii.sql.ActiveRecord.findAll(): Возвращает массив или объект нескольких Active Record, соответствующие строкам результата запроса.

    Оба метода принимают первый агрумент следующего формата:
    • Скалярное значение: значение рассматривается как значение первичного ключа, по которому идет поиск. Первичный ключ определяется автоматически из схемы БД.
    • Массив скалярных значений: массив рассматривается как значения первичного ключа, по которым идет поиск. — Объект, ключами которого являются имена столбцов, а значения соответствуют значениям столбцов, по которым идет поиск.

    Следующие примеры показывают как эти методы могут быть использованы:

    // Возвращает клиента с ID 123
    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            // ...
        });
    
    // Возвращает клиентовс ID 100, 101, 123 или 124
    // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
    app.models.Customer
        .findAll([100, 101, 123, 124])
        .then(function(customers) {
            // ...
        });
    
    // Возвращает активного клиента с ID 123
    // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
    app.models.Customer
        .findOne({
            id: 123,
            status: app.models.Customer.STATUS_ACTIVE
        })
        .then(function(customer) {
            // ...
        });
    
    // Возвращает всех неактивных клиентов
    // SELECT * FROM `customer` WHERE `status` = 0
    app.models.Customer
        .findAll({
            status: app.models.Customer.STATUS_INACTIVE
        })
        .then(function(customers) {
            // ...
        });
    

    Примечание: Ни Jii.sql.ActiveRecord.findOne(), ни Jii.sql.ActiveQuery.one() не добавят `LIMIT 1` в SQL выражение. Если Ваш запрос действительно может вернуть множество данных, то необходимо вызвать `limit(1)` для установки предела, например `app.models.Customer.find().limit(1).one()`.

    Вы можете так же использовать обычные SQL запросы для получения данных и заполнениях их в Active Record. Для этого необходимо использовать метод Jii.sql.ActiveRecord.findBySql():

    // Возвращает всех неактивных клиентов
    var sql = 'SELECT * FROM customer WHERE status=:status';
    app.models.Customer
        .findBySql(sql, {':status': app.models.Customer.STATUS_INACTIVE})
        .all()
        .then(function(customers) {
            // ...
        });
    

    Не вызывайте методы создания запроса после вызова Jii.sql.ActiveRecord.findBySql(), они будут игнорироваться.

    Доступ к данным



    Как упоминалось выше, экземпляры Active Record заполняются данными из результатов SQL запроса, и каждая строка результата запроса соответствует одному экземпляру Active Record. Вы можете получить доступ к значениям столбцов через атрибуты Active Record, например,

    // Имена столбцов "id" и "email" из таблицы "customer"
    app.models.Customer
    .findOne(123)
    .then(function(customer) {
    var id = customer.get('id');
    var email = customer.get('email');
    });
    

    Получение данные в объектах


    Получение данные как Active Record удобно, но иногда это может быть неоптимально из-за большого потребления памяти, которое расходуется на создание экземпляров Active Record. В этом случае вы можете получить их как обычные объекты, для этого нужно вызвать метод Jii.sql.ActiveQuery.asArray().
    По факту, в JavaScript вы получите массив, наполненный объектами. Поэтому правильней было бы назвать метод asObject(), и такой метод (синоним) есть. Но для сохранения API Yii 2 оставлен метод asArray().

    // Возвращает всех клиентов, каждый из которых
    // представлен как объект
    app.models.Customer.find()
        .asArray() // alias is asObject()
        .all()
        .then(function(customers) {
            // ...
        });
    

    Сохранение данных


    Изпользуя Active Record, Вы можете сохранять данные в БД выполнив следующие шаги:
    1. Получите или создайте экземпляр Active Record;
    2. Задайте новые значение атрибутам
    3. Вызовите метод Jii.sql.ActiveRecord.save() для сохранение данных.

    Например,

    // Добавление новой строки в таблицу
    var customer = new app.models.Customer();
    customer.set('name', 'James');
    customer.set('email', 'james@example.com');
    customer.save().then(function(success) {
    
        return app.models.Customer.findOne(123);
        }).then(function(customer) {
    
            // Обновление данных
            customer.set('email', 'james@newexample.com');
            return customer.save();
        }).then(function(success) {
            // ...
        });
    

    Метод Jii.sql.ActiveRecord.save() может либо добавить, либо обновить данные строки, в зависимости от состояния Active Record. Если экземплер был создан с помощью оператора `new`, то метод добавит новую строку. Если экземпляр получен через метод find() и ему подобные или уже был вызван метод save() ранее, то метод save()
    обновит данные.

    Валидация данных


    Класс Jii.sql.ActiveRecord наследуется от Jii.base.Model, поэтому в нем доступна валидация данных. Вы можете задать правила вализации через переопределение метода Jii.sql.ActiveRecord.rules() и проверить на правильность значений через метод Jii.sql.ActiveRecord.validate().

    Когда вы вызываете метод Jii.sql.ActiveRecord.save(), по-умолчанию, автоматически будет вызван метод Jii.sql.ActiveRecord.validate(). Только проверенные данные должны сохраняться в БД; Если данные не верны, то метод вернет `false` и Вы можете получить ошибку через метод Jii.sql.ActiveRecord.getErrors() или ему подобные.

    Изменение множества атрибутов


    Как и обычные модели, экземпляр Active Record так же поддерживает изменение атрибутов через передачу объекта. Используя это способ, вы можете присвоить значения нескольких атрибутов Active Record через вызов одного метода. Помните, что только безопасные атрибуты могут быть массово присвоены.

    var values = {
        name: 'James',
        email: 'james@example.com'
    };
    
    var customer = new app.models.Customer();
    
    customer.setAttributes(values);
    customer.save();
    

    Измененные атрибуты


    Когда вы вызываете метод Jii.sql.ActiveRecord.save(), происходит сохранение только измененных аттрибутов Active Record. Атрибут считается измененным, если было изменено его значение. Обратите внимание, что проверка данных будет выполняться независимо от существования измененных атрибутов.

    Active Record автоматически сохраняет список измененных атрибутов. Она сохраняет старые версии атрибутов и сравнивает их с последней версией. Вы можете получить измененные атрибуты через метод Jii.sql.ActiveRecord.getDirtyAttributes().

    Для получения старых значений атрибутов, вызывайте метод Jii.sql.ActiveRecord.getOldAttributes() или Jii.sql.ActiveRecord.getOldAttribute().

    Значения по-умолчанию


    Некоторые из ваших столбцов в таблице могут иметь значения по умолчанию, определенные в базе данных. Вы можете предварительно заполнить Active Record этими значениями, вызвав метод Jii.sql.ActiveRecord.loadDefaultValues(). Этот метод синхронный, т.к. схема БД заранее подгружается при открытии соединения.

    var customer = new app.models.Customer();
    customer.loadDefaultValues();
    // customer.get('xyz') Значение атрибута `xyz` будет соответсвовать значению по-умолчанию для столбца `xyz`.
    

    Обновление нескольких строк


    Описанные выше методы работают с экземплярамм Active Record. Чтобы обновить несколько строк одновременно, вы можете вызвать статический метод Jii.sql.ActiveRecord.updateAll():

    // UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
    app.models.Customer.updateAll({status: app.models.Customer.STATUS_ACTIVE}, {'like', 'email', '@example.com'});
    

    Удаление данных


    Для удаления строки из таблицы, необходимо у эксемпляра Active Record, соответствующей этой строке, вызвать метод Jii.sql.ActiveRecord.delete().

    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            customer.delete();
        });
    

    Вы можете вызвать статический метод Jii.sql.ActiveRecord.deleteAll() для удаления множества строк по условию.
    Например,

    app.models.Customer.deleteAll({status: app.models.Customer.STATUS_INACTIVE});
    

    Работа с связанными данными



    Помимо работы с отдельными таблицами базы данных, Active Record способен связать данные через первичные данные. Например, данные о клиентах могут быть связанны с заказами. При объявлении соответствующей связи в Active Record, Вы можете получить информацию о заказе клиента, используя выражение `customer.load('orders')`, при этом на выходе вы получите массив экземпляров `app.models.Order`.

    Объявление зависимостей


    Для работы с реляционными данными при помощи Active Record, сначала нужно объявить отношение в классе Active Record. Например,

    /**
    * @class app.models.Customer
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Customer', /** @lends app.models.Customer.prototype */{
    
        // ...
    
        getOrders: function() {
            return this.hasMany(app.models.Order.className(), {customer_id: 'id'});
        }
    
    });
    
    /**
    * @class app.models.Order
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Order', /** @lends app.models.Order.prototype */{
    
        // ...
    
        getCustomer: function() {
            return this.hasOne(app.models.Customer.className(), {id: 'customer_id'});
        }
    
    });
    

    В приведенном выше коде, мы объявили соотношение `orders` для класса `app.models.Customer`, и отношение `customer` для класса `app.models.Order`.

    Каждый метод отношение должен быть назван как `getXyz` (get + имя отношения с первой буквой в нижнем регистре). Обратите внимание, что имена отношений являются *чувствительными к регистру*.

    В отношении, вы должны указать следующую информацию:

    • Кратность связи: указывается при вызове методов Jii.sql.ActiveRecord.hasMany() или Jii.sql.ActiveRecord.hasOne(). В приведенном выше примере у клиента много заказов, а у заказа только один клиент.
    • Название связанного класса Active Record: указывается в качестве первого параметра у выше названных методов. Рекомендуется получать имя класса через `Xyz.className()`, чтобы, во-первых, проверить существование класса еще на этапе конструирования отношения, а во-вторых, чтобы IDE подсказывала Вам имя класса при написании.
    • Связь между двумя схемами таблиц: определяет столбец (ы), через который связаны два типа данных. Значениями объекта являются столбцы первичных данных, а ключами — столбцы связанных данных.

    Доступ к связанным данным


    После объявления отношения, вы можете получить доступ к связанным данным через имя отношеня. Если Вы уверены, что связанные данные уже подгружены в Active Record, то можно получить связанные Active Record аналогично доступу к свойствам объекта через метод get(). Иначе, лучше использовать метод Jii.sql.ActiveRecord.load() для загрузки связанных данных, который будет всегда возвращать объект `Promise`, но не будет слать лишний запрос в БД, если связь уже была подгружена ранее.

    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            // SELECT * FROM `order` WHERE `customer_id` = 123
            return customer.load('orders');
        })
        .then(function(orders) {
            // orders - массив экземпляров класса `app.models.Order`
        });
    

    Если отношение объявлено методом Jii.sql.ActiveRecord.hasMany(), то данные отношения будут представлены массивом экземпляров Active Record (или пустым массивом). Если методом Jii.sql.ActiveRecord.hasOne(), то данные отношения будут представлены экземпляром Active Record или `null`, если данные отсутствуют.

    При доступе к отношению в первый раз, будет выполнен SQL запрос в БД, как показано в примере выше. При повторном обращении, запрос выполняться не будет.

    Отношения через дополнительную таблицу (Junction Table)


    При моделировании баз данных, когда связь между двумя таблицами Many-Many, то обычно добавляется дополнительная таблица — Junction Table. Например, таблица `order` и таблица `item` могут быть связаны с помощью таблицы `order_item`.

    При объявлении таких отношений, Вам нужно вызвать методы Jii.sql.ActiveQuery.via() или Jii.sql.ActiveQuery.viaTable() с указанием дополнительный таблицы. Разница между этими методами в том, что первый указывает таблицу перехода с точки зрения текущего имени отношения, в то время как последний непосредственно дополнительную таблицу. Например,

    /**
    * @class app.models.Order
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Order', /** @lends app.models.Order.prototype */{
    
        // ...
    
        getItems: function() {
            return this.hasMany(app.models.Item.className(), {id: 'item_id'})
                .viaTable('order_item', {order_id: 'id'});
        }
    
    });
    

    или альтернативно,

    /**
    * @class app.models.Order
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Order', /** @lends app.models.Order.prototype */{
    
        // ...
    
        getOrderItems: function() {
            return this.hasMany(app.models.OrderItem.className(), {order_id: 'id'});
        },
    
        getItems: function() {
            return this.hasMany(app.models.Item.className(), {id: 'item_id'})
                .via('orderItems');
        }
    
    });
    

    Использование отношений, объявленных с дополнительной таблицей, аналогично обычным отношениям. Например,

    // SELECT * FROM `order` WHERE `id` = 100
    app.models.Order
        .findOne(100)
        .then(function(order) {
    
            // SELECT * FROM `order_item` WHERE `order_id` = 100
            // SELECT * FROM `item` WHERE `item_id` IN (...)
            return order.load('items');
        })
        .then(function(items) {
            // items - массив экземпляров `app.models.Item`
        });
    

    Ленивая Загрузка и жадная загрузка


    В разделе доступа к связанным данным, мы рассказывали, что вы можете получить доступ к отношению из Active Record через методы get() или load(). SQL запрос будет отправлять в БД только при первом обращении к связанным данным. Такие способы загрузки данных назваются ленивыми (lazy loading).
    Например,

    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            // SELECT * FROM `order` WHERE `customer_id` = 123
            customer.load('orders').then(function(orders) {
    
                // SQL запрос не отправляется
                return customer.load('orders');
            }).then(function(orders2) {
    
                // После подгрузки связей, они доступны также через метод <i>get()</i>.
                var orders3 = customer.get('orders');
            });
        });
    
    

    Ленивый нагрузка очень удобна в использовании. Тем не менее, это может вызывать проблемы с производительностью, когда вам нужно получить доступ к связанным занным для множества экземпляров Active Record. Рассмотрим следующий пример кода, сколько SQL запросов будет выполнено?

    // SELECT * FROM `customer` LIMIT 100
    app.models.Customer.find()
        .limit(100)
        .all()
        .then(function(customers) {
            return Promise.all(customers.map(function(customer) {
    
                // SELECT * FROM `order` WHERE `customer_id` = ...
                return customer.load('orders');
            }));
        }).then(function(result) {
            var firstOrder = result[0][0];
            // ...
        });
    

    В данном примере выполнится 101 SQL запрос! Потому что для каждого клиента будут получены заявки через отдельный запрос. Чтобы решить эту проблему производительности, можно использовать подход *жадной загрузки*, как показано ниже,

    // SELECT * FROM `customer` LIMIT 100;
    // SELECT * FROM `orders` WHERE `customer_id` IN (...)
    app.models.Customer.find()
        .with('orders')
        .limit(100)
        .all()
        .then(function(customers) {
            customers.forEach(function(customer) {
    
                // без SQL запроса
                var orders = customer.get('orders');
            });
        });
    

    Вы можете загрузить вместе с основной записью одно или несколько отношений. Вы даже можете загрузить сразу и вложенные отношения. Например, если `app.models.Customer` связан с` app.models.Order` через отношение `orders`, а `app.models.Order` связан с `Item` через `items`. При запросе `app.models.Customer`, вы можете сразу загрузить отношение `items` указав в методе with() `orders.items`.

    Следующий код показывает различные использование Jii.sql.ActiveQuery.with(). Мы предполагаем, что класс `app.models.Customer` имеет два отношения: `orders` и `country`, в то время как класс `app.models.Order` имеет одно соотношение — `items`.

    // Принудительная загрузка отношений "orders" и "country"
    app.models.Customer.find()
        .with('orders', 'country')
        .all()
        .then(function(customers) {
            // ...
        });
    
    // это эквивалентно записе через массив
    app.models.Customer.find()
        .with(['orders', 'country'])
        .all()
        .then(function(customers) {
            // без SQL запроса
            var orders = customers[0].get('orders');
            var country = customers[0].get('country');
        });
    
    // Принудительная загрузка отношения "orders" и вложенного отношения "orders.items"
    app.models.Customer.find()
        .with('orders.items')
        .all()
        .then(function(customers) {
    
            // Получение пунктов из первого заказа для первого клиента
            // без SQL запроса
            var items = customers[0].get('orders')[0].get('items');
        });
    

    Вы можете загрузить принудительно глубоко вложенные отношения, такие как `a.b.c.d`. Все родительские отношения будут принудительно загружены.

    При жадной загрузке отношений, вы можете настроить соответствующий запрос, передав анонимную функцию. Например,

    // Поиск клиентов вместе с их странами а активными заказами
    // SELECT * FROM `customer`
    // SELECT * FROM `country` WHERE `id` IN (...)
    // SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
    app.models.Customer.find()
        .with({
            country: 'country',
            orders: function (query) {
                query.andWhere({'status': app.models.Order.STATUS_ACTIVE});
            }
        })
        .all()
        .then(function(customers) {
            // ...
        });
    

    При настройке реляционного запроса для связи, вы должны указать имя отношения в качестве ключа объекта и использовать анонимную функцию как значение соответствующего объекта. Первым агрументом нонимной функции будет параметр `query`, который представляет собой объект Jii.sql.ActiveQuery. В примере выше, мы изменяем запроса путем добавления дополнительного условия о статусе заказа.

    Обратные отношения


    Отношения между классами Active Record зачастую обратно связаны друг с другом. Например, класс `app.models.Customer` связан с `app.models.Order` через отношение `orders`, а класс `app.models.Order` обратно связан с классом `app.models.Customer` через отношение `customer`.

    /**
    * @class app.models.Customer
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Customer', /** @lends app.models.Customer.prototype */{
    
        // ...
    
        getOrders: function() {
            return this.hasMany(app.models.Order.className(), {customer_id: 'id'});
        }
    
    });
    
    /**
    * @class app.models.Order
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Order', /** @lends app.models.Order.prototype */{
    
        // ...
    
        getCustomer: function() {
            return this.hasOne(app.models.Customer.className(), {id: 'customer_id'});
        }
    
    });
    

    Теперь рассмотрим следующий фрагмент кода:

    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            // SELECT * FROM `order` WHERE `customer_id` = 123
            return customer.load('orders');
        }).then(function(orders) {
            var order = orders[0];
    
            // SELECT * FROM `customer` WHERE `id` = 123
            return order.load('customer');
        }).then(function(customer2) {
    
            // Отображает "not the same"
            console.log(customer2 === customer ? 'same' : 'not the same');
        });
    

    Мы предполагаем, что объекты `customer` и `customer2` являются одинаковыми, но на самом деле это не так. Они содержат одинаковые данные на являются разными экземплярами. При доступе к `order.customer` выполняется дополнительный SQL запрос для получения нового объекта `customer2`.

    Чтобы избежать избыточного выполнения последнего SQL запроса в приведенном выше примере, мы должны указать, что `customer` является *обратной зависимостью* от `orders` с помощью метода Jii.sql.ActiveQuery.inverseOf().

    /**
    * @class app.models.Customer
    * @extends Jii.sql.ActiveRecord
    */
    Jii.defineClass('app.models.Customer', /** @lends app.models.Customer.prototype */{
    
        // ...
    
        getOrders: function() {
            return this.hasMany(app.models.Order.className(), {customer_id: 'id'}).inverseOf('customer');
        }
    
    });
    

    После этих изменений мы получим:

    // SELECT * FROM `customer` WHERE `id` = 123
    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            // SELECT * FROM `order` WHERE `customer_id` = 123
            return customer.load('orders');
        }).then(function(orders) {
            var order = orders[0];
    
            // SELECT * FROM `customer` WHERE `id` = 123
            return order.load('customer');
        }).then(function(customer2) {
    
            // Отображает "same"
            console.log(customer2 === customer ? 'same' : 'not the same');
        });
    

    Замечание: Обратные отношения не работают для отношений Many-Many, объявленные с дополнительной таблцей (Junction Table).

    Сохранение зависимостей



    При работе с реляционными данными, часто необходимо добавить отношения между различными данными или удалить существующие отношения. Для этого нужно установить правильные значения для столбцов, которые определяют отношения. С применением Active Record Вы можете сделать это примерно так:

    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            var order = new app.models.Order();
            order.subtotal = 100;
    
            // ...
    
            // Устанавливаем значение, определяющее отношение "customer" для `app.models.Order` и сохраняем.
            order.customer_id = customer.id;
            order.save();
        });
    

    Active Record предоставляет метод Jii.sql.ActiveRecord.link(), который позволяет сделать это более изящно:

    app.models.Customer
        .findOne(123)
        .then(function(customer) {
            var order = new app.models.Order();
            order.subtotal = 100;
    
            // ...
    
            order.link('customer', customer);
        });
    

    Метод Jii.sql.ActiveRecord.link() ожидает имя отношения и экзепмляр Active Record, с которым должно соединена запись. Метод соединит два экземпляра Active Record и сохранит их в БД. В приведенном выше примере, он установит атрибут `customer_id` в `app.models.Order`.
    Примечание: Вы не можете связать два только что созданных экземпляров Active Record.

    Выгода от использования метода Jii.sql.ActiveRecord.link() еще более очевидна, когда отношение определяется с помощью дополнительной таблицей. Например, вы можете использовать следующий код, чтобы связать экземпляр `app.models.Order` с `app.models.Item`:

    order.link('items', item);
    

    Этот код автоматически добавит строку в таблицу `order_item` для создания связи.

    Для удаления связи между двумя экземплярами Active Record, используйте метод Jii.sql.ActiveRecord.unlink()|unlink().
    Например,

    app.models.Customer.find()
        .with('orders')
        .all()
        .then(function(customer) {
            customer.unlink('orders', customer.get('orders')[0]);
        });
    

    По умолчанию, метод Jii.sql.ActiveRecord.unlink() метод устанавливает значения ключа, определяющего отношение, в `null`. Однако, вы можете передать параметр `isDelete` как `true`, чтобы удалить строки с таблицы.

    В заключении




    На данный момент в Active Record от Jii не реализованы транзакции, вещь нужная и поэтому в будущем она появится.
    Как я уже говорил в предыдущей статье, Jii — опенсорсный проект, поэтому я буду очень рад, если кто-то присоединится к разработке Jii. Пишите на affka@affka.ru.

    Сайт фреймворка — jiiframework.ru
    GitHub — github.com/jiisoft

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Как вы оцениваете идею Jii?

    Какой из разделов Jii описать в следующей статье?

    Поделиться публикацией

    Комментарии 13

      +1
      Спасибо за труд! Всегда любил Yii и javascript, а теперь они еще и вместе =)))
      И вообще отрадно что земляк сотворил такое =)))
        0
        Еще один вопрос. Откуда лучше форкать из jiisoft или из твоего аккаунта, для того чтобы контрибьютить?
        Вопрос снят =))
        +1
        Проделанная работа восхищает. Огромное спасибо! По-моему yii популярен не только потому что прост, но и за его модульность и большой базы готовых расширений (http://www.yiiframework.com/extensions/). Будем ждать подобного, чтоб каждый мог поделиться и взять что-то.
          0
          Количество расширений и вообще готовых решений во многом зависит от сообщества. Надеюсь, что оно появится :) Ну а я буду этому сопутствовать)
            0
            Из готового я бы с руками забрал валидацию данных форм, что удобно в данном подходе — код один, как на серверной стороне, так и на клиентской. Получается достаточно сделать события как реагировать на ошибки на клиентской стороне и как на серверной, а потом добавлять по желанию любые валидаторы для данных, которые будут работать и там и там.

            Количество расширений и вообще готовых решений во многом зависит от сообщества.
            Сообщество ведь должно куда-то добавлять свой говнокод в виде расширений? Или пока весь комбайн надо брать и убирать что не надо? =) Я в своем посте выше об этом говорил, что хорошо бы какую базу организовать, а там наверно уже больше шансов, что добавят что-то.

            А так бы я рад помочь в коде, но с JS у меня пока отношения сомнительного характера =) Если будет лозунг что-то вроде «ГовноКод лучше, чем вообще никакого кода =)» то мог бы написать временные решения, которые по возможности более бородатые дядьки перепишут по необходимости.
              0
              Правила валидаций уже реализованы и большинство валидаторов уже тоже. В одной из следующих статей опишу как ими пользоваться (как на клиенте, так и на сервере).

              > Сообщество ведь должно куда-то добавлять свой говнокод в виде расширений?
              Базу получается самому нужно написать, на что у меня мало времени. Возможно стоит выложить создать на сайте раздел «Расширения», где опубликовать инструкцию по их созданию (как лучше именовать, чего придерживаться и прочие советы).

              > Если будет лозунг что-то вроде «ГовноКод лучше, чем вообще никакого кода =)»
              Я сам могу нормальный код писать, но на это нужно время. Но вообще в большинстве плагинах/расширениях любого продукта всегда много говнокода, но главное что есть и качественные расширения.
                0
                Правила валидаций уже реализованы и большинство валидаторов уже тоже. В одной из следующих статей опишу как ими пользоваться (как на клиенте, так и на сервере).

                Это замечательно! Будем ждать.
                Базу получается самому нужно написать, на что у меня мало времени. Возможно стоит выложить создать на сайте раздел «Расширения», где опубликовать инструкцию по их созданию (как лучше именовать, чего придерживаться и прочие советы).

                Как раз о втором я и говорил, чтоб любой мог выложить.
                Я сам могу нормальный код писать, но на это нужно время. Но вообще в большинстве плагинах/расширениях любого продукта всегда много говнокода, но главное что есть и качественные расширения.
                по-моему, нормальный код в первую очередь должен быть в ядре, а вот модули/Расширения можно отдать
                таким как я, хотя бы будут «временные решения», а после уже по возможности или с 0 переписать можно или поправить явные недочеты, при этом имея уже приличный фидбэк к тому или иному расширению
          0
          Выглядит очень круто, надеюсь хватит терпения довести все это до стабильного релиза.
            +1
            Спасибо! Буду стараться) Как никак Jii живет уже больше двух лет :)
            0
            А почему не ES6 модули и классы? Зачем все файлы подключать? Зачем все в одном, когда в npm и так огромное количество готовых и удобноых библиотек/фреймворков.
              0
              Почему не es6 — отвечал тут github.com/jiisoft/jii/issues/1
              Jii — это не все в одном. В нем модульный подход. Если зайти на гитхаб, то можно увидеть множество репозиториев, каждый из которых доступен в npm и ставится отдельно. Поэтому, например, можно подключить Active Record пакет (jii-ar-sql) и использовать его даже без создания jii приложения.
              +1
              Даешь Jaravel, Jymfony, JakePHP! :)
                0
                Друзья, присоединяйтесь к обсуждению фич и процесса разработки — github.com/jiisoft/jii/issues/10

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое