Публикация

CustomSQLColumn: автоматическое предотвращение ошибок, связанных с использованием администрируемых таблиц

Порой возникает необходимость в использовании CustomSQLColumn: не всякий запрос можно построить с помощью обычных сервисов. Ввести небольшое условие, или какой-нибудь расчет, для которого писать «хранимку» нецелесообразно, а вписать его в CustomSQLColumn удобно. Скажем, выводить крестик, если два других поля равны, а если нет – выводить пробел. Особенно в отчётах часто возникает необходимость в поле этого типа. И вот, отчёт создан, отлажен, и вдруг, тестируя уже под пользователем, выясняется, что администрируемые таблицы ему не видны. Поминаются недобрыми словами склероз и забывчивость, когда возникают ошибки вроде «Ошибка открытия источника данных "". Оригинальное сообщение об ошибке: ORA-01031: insufficient privileges», или что-то подобное. Вместо администрируемых таблиц необходимо ставить представления, т.е. view.

Помня об этой особенности, обычно используют в тексте CustomSQLColumn указание на представление, а не на таблицу. Тогда для пользователей-администраторов следует делать подмену view на table. По методу, предложенному Андреем Громовым, на событие BeforeDatasetOpen выполняются проверки:

// Пример для CustomSQLFilter
function ds_OfferingAllowRedemptionOnDatasetBeforeOpen(Dataset, DoOpen) {
        if (Connector.CurrentUser.IsAdmin) {
                var SelectQuery = Dataset.SelectQuery;
                var Filters = SelectQuery.Items(0).Filters;
                var TablePrefix = 'tbl_';
                var Filter;
                var Count = Filters.Count;
                for(var i = 0; i Count; i++) {
                        Filter = Filters.Items(i);
                        if (Filter.FilterType == ftCustomSQL) {
                                Filter.SQLText = Filter.SQLText.replace(/vw_/g, TablePrefix);
                        }
                }
        }
}  

// Пример для CustomSQLColumn
function ds_OfferingOnDatasetBeforeOpen(Dataset, DoOpen) {
        if (Connector.CurrentUser.IsAdmin) {
                var SelectQuery = Dataset.SelectQuery;
                var Select = SelectQuery.Items(0);
                var TablePrefix = 'tbl_';
                var Columns = Select.Columns;
                var Column;
                var Count = Columns.Count;
                for(var i = 0; i Count; i++) {
                        Column = Columns.Items(i);
                        if (Column.ColumnType == ctSQLText) {
                                Column.SQLText = Column.SQLText.replace(/vw_/g, TablePrefix);
                        }
                }
        }
}

Метод замечательно работает, когда CustomSQLColumnы содержат указания на таблицы разделов, администрируемость которых всем известна (вроде tbl_Account или tbl_Contact). Но как быть, когда администриуются не только разделы, но и некоторые детали? Да ещё (как это иногда бывает), деталь сделали администрируемой уже после того, как был создан отчёт. Или во время разработки сервиса SelectQuery просто забыли об этой особенности, и при создании CustomSQLColumn-ов использовали имена таблиц, а не представлений. Отлавливать появившиеся ошибки в куче специфических отчётов, SelectQuery которых содержат десятки CustomSQLColumn было бы крайне медленным и неэффективным занятием. К тому же, ручная правка может наплодить новых ошибок, в чём я очень быстро убедился.

Предлагаемые выше методы оказались неприменимы. Пришлось разработать свой. Нужна была автоматическая проверка доступности всех таблиц, используемых в CustomSQLColumn-ах, и их автоматическая замена. Первоначально было необходимо просто выяснить, из-за каких именно CustomSQLColumn и упоминающихся в них таблиц происходит «свал». Зная, какие именно CustomSQLColumn-ы вызывают ошибку, было куда легче её исправить. Но впоследствии задача была усложнена: раз машина знает, из-за каких таблиц и полей SelectQuery не отработает, так пусть сама их и заменит на представления. После остаётся только передать предлагаемой функции SelectQuery перед открытием: все возможные ошибки, связанные с появлением представлений вместо таблиц будут исправлены автоматически. Функция работает на удивление быстро, и благодаря ей можно пользоваться CustomSQLColumn-ами без опасений, что какая-то из таблиц под пользователем превратится в представление.
Способы, которыми осуществляется проверка, возможно, не самые оптимальные, но подход, полагаю, правильный:

function FindUnsufficientTables(SQ, IsNeededReplace){
//Функция ищет недоступные таблицы и представления в CustomeSQLColumnах
//и в зависимости от аргумента IsNeededReplace выводит их список в лог (false)
//или автоматически производит замену (true)
//В качестве первого аргумента используется Select Query, где требуется провести поиск/замену
       
        var TablesHash = {};//Ключ  - имя таблицы. Значение - имя колонки, через запятую.  
        // Символы ограничителей имени таблицы:
        var QuoteLeft = '"';//Для Оракла - ", для MSSQL - [
        var QuoteRight = '"';//Для Оракла - ", для MSSQL - ]       

        // Также их можно определить автоматически:
        /*
        if (Connector.DBEngine.DBEngineTypeCode == 'Oracle'){
                var QuoteLeft = '"';
                var QuoteRight = '"';
        } else {
                var QuoteLeft = '[';
                var QuoteRight = ']';
        }
        */


        var Prefix = Connector.CurrentUser.IsAdmin?('vw_'):('tbl_');
        var ReplacePrefix = Connector.CurrentUser.IsAdmin?('tbl_'):('vw_');
       
        var Select = SQ.Items(0);
    var Columns = Select.Columns;
    var Column;
    var Count = Columns.Count;
    for(var i = 0; i Count; i++) {
                Column = Columns.Items(i);
                if (Column.ColumnType == ctSQLText) { // ctSQLText  - константа из scr_SysEnums
                        var TablesDraftSplit = Column.SQLText.split(QuoteLeft + Prefix);
                        for (var j = 1; j TablesDraftSplit.length; j++){
                                var TablesSplit = TablesDraftSplit[j].split(QuoteLeft);
                                if (Select.Joins.ItemsByLeftTableAlias(Prefix + TablesSplit[0]) == null &&
                                        Select.FromTableAlias != Prefix + TablesSplit[0]){
                                //Если таблица не входит в число алиасов join-oв и основной таблицы
                                        if (!!TablesHash[TablesSplit[0]]){
                                                if (TablesHash[TablesSplit[0]].indexOf(Column.ColumnAlias) 0 ){
                                                //Добавляем название колонки только если такого ещё нет.
                                                        TablesHash[TablesSplit[0]] = TablesHash[TablesSplit[0]] + Column.ColumnAlias + ', ';
                                                }
                                        } else {
                                                TablesHash[TablesSplit[0]] = Column.ColumnAlias +', ';
                                        }
                                }
                        }
                }
        }              

        //TablesHash содержит список всех используемых таблиц.
        //Удаляем лишнюю запятую:

    for (var key in TablesHash){
            TablesHash[key] = TablesHash[key].substr(0, TablesHash[key].length - 2);
    }

        //Проверяем доступность каждой таблицы:
        for (var key in TablesHash){
                //здесь используется проверка "в лоб", можно ли читать из этой таблицы.
                //Наверняка существует более грамотный вариант.
                var SQL = 'select * from ' + QuoteLeft + Prefix + key + QuoteRight;
                try {
                        Connector.DBEngine.ExecuteCustomSQL(SQL, System.EmptyValue);
                } catch (e) {
                        if (IsNeededReplace){
                                var RegExpString = QuoteLeft + Prefix + key + QuoteRight;
                                var Reg = new RegExp(RegExpString, "g");
                                var ColAliasSplit = TablesHash[key].split(', ');
                                var ColAliasSplitLength = ColAliasSplit.length;
                                for (var i = 0; i ColAliasSplitLength; i++){
                                //для каждого перечисленного поля
                                        SQ.Columns.ItemsByAlias(ColAliasSplit[i]).SQLText =
                                                SQ.Columns.ItemsByAlias(ColAliasSplit[i]).SQLText.replace(Reg,
                                                QuoteLeft + ReplacePrefix + key + QuoteRight);
                                }
                        } else {
                                Log.Write(1, 'Недоступна таблица ' + Prefix + key +
                                        ' (используется в колонках ' + TablesHash[key] + ')');
                        }
                }
        }      
}

Нравится

Поделиться

1 комментарий

"Будак Анатолий Васильевич" написал://здесь используется проверка "в лоб", можно ли читать из этой таблицы.

Так можно же проверить возможность чтения из таблицы по-честному. Метод у Connector.CurrentUser если есть TableGroup у таблицы.

Показать все комментарии