Основная мысль многих постов Мартина Фаулера, повторяемых в его книге, заключается в том, что NoSQL представляет собой движение от интеграции к базе данных и к инкапсуляции баз данных в приложениях и использованию сервисов для интеграции данных. Тем не менее, тот факт, что это не тот способ, которым всегда используются высокопроизводительные РСУБД, говорит о рынке больше, чем сама технология РСУБД. В течение последних пяти лет я строил реляционные базы данных по аналогичному подходу, используя веб-сервисы как источник вдохновения для интерфейсов. В этом посте обсуждается несколько разных подходов к этому на
PostgreSQL . Обратите внимание, что другие РСУБД могут отличаться во многих из этих аспектов.
Обещание модели реляционной базы данных
Реляционные базы данных предназначены для хранения информации в нейтральных форматах приложений и последующего представления ее различным приложениям различными способами. Цель, как говорит Фаулер, «интеграция базы данных». Чтобы облегчить достижение этой цели удобным способом, большинство СУБД поставляются с солидным набором функций, предназначенных для инкапсуляции физического хранилища за специфичным для приложения представлением хранимых данных. К ним относятся представления, функции и хранимые процедуры. Эти функции позволяют инженеру базы данных создавать приложения, которые инкапсулируют физическое хранилище, так что система управления базами данных, по существу, предоставляет информационную модель через множество клиентских интерфейсов. Это позволяет добиться того, что Фаулер защищает, а именно: инкапсулировать базу данных в приложение,но он допускает декларативный доступ к этому API. По крайней мере, теоретически, нет никакой разницы между тем, что Фаулер защищает для NoSQL, и тем, что многие администраторы баз данных делают с RDBMS. Это также говорит о том, что Фаулер занимается чем-то важным, но что отказ от СУРБД во многих случаях не может быть ответом.
Почему СУБД редко выполняет это обещание
Однако, несмотря на всю теорию, большинство СУБД редко используются таким образом. Я думаю, что есть несколько факторов, которые обычно влияют на такую инкапсуляцию:
- Инвестиции (затраты на экспертизу и лицензию) в конкретные СУБД ИТ-отделами означают, что разработчики приложений хотят писать переносимый код и, следовательно, писать код с наименьшим общим знаменателем
- Разработчики приложений часто хотят, чтобы весь доступ проходил через их базы данных, и, следовательно, не хотят централизовать логику приложения в базе данных, и
- Многие из стандартов гарантируют, что инструменты не являются действительно оптимальными для этой задачи. Лучшие инструменты — это те, которые выходят за рамки стандартов, и, следовательно, имеют большее значение в отношении портативности.
Первый рыночный фактор связан с тем фактом, что системы управления реляционными базами данных являются дорогостоящими системами в нескольких смыслах. В дополнение к (зачастую огромным) затратам на лицензирование у вас есть необходимость нанимать людей с опытом и / или обучать их. Компании обычно решают эту проблему, объединяя свой опыт и серверы в одной СУБД. Таким образом, вы видите магазины Oracle, магазины SQL Server, магазины DB2 и т. Д. Это оставляет блокировку в качестве одной из основных затрат для предприятий и уменьшает рынок для приложений, специфичных для баз данных. Поэтому разработчики приложений довольно рационально чаще выбирают написание кода SQL, который выполняется в любой другой СУБД. Это, естественно, требует отказа от расширенных функций, таких как Oracle Objects или функции pl / pgsql для ванильного SQL.
Проблема, конечно, в том, что трудно инкапсулировать данные, когда вы ограничены самым базовым набором функций. Хотя представления могут работать, их обновляемость может отличаться и иметь разные последствия для каждой СУБД. Хранимые процедуры или функции намного хуже в этом отношении. Следовательно, необходимость написания переносимого кода требует по существу отказа от самих инструментов, используемых для инкапсуляции данных. По этой причине я не думаю, что вы можете написать как базу данных для использования несколькими приложениями (т. Е. Там, где внутренние структуры данных инкапсулированы), так и написать базу данных, которая работает на нескольких RDBMS. Вы должны выбрать. Разработчики не могут быть обвинены в выборе варианта, который дает их приложениям наибольшую рыночную привлекательность.
Конкурирующие желания блокировки приложений — еще один фактор. Поставщики СУБД обычно хотят ограничить доступ к определенному количеству клиентских лицензий или рабочих мест, и если соединения могут быть объединены в промежуточное программное обеспечение, это может помочь обойти некоторые из этих ограничений (это может вызвать договорные проблемы в зависимости от характера EULA, но таким образом можно легко обойти технические элементы управления, и в какой-то момент у вас возникают проблемы с определениями, особенно когда системы слабо связаны). Разработчики приложений хотят продавать свои собственные лицензии, и это может быть сделано, только если соединения проверены на прикладном уровне. Поэтому многие разработчики приложений противоречат интересам инкапсулированных схем баз данных. Вместо,СУБД используется в основном как частное хранилище данных с некоторыми дополнительными возможностями отчетности.
Некоторые поставщики РСУБД фактически оптимизируют свои системы для вышеуказанных потребностей. Одна из возможностей, предлагаемых SQLAnywhere, заключается в том, что такие разработчики, как Intuit, могут заблокировать базу данных для определенного приложения, запретив всем сторонний доступ, и большая часть нынешней популярности MySQL может быть связана с тем фактом, что он хорошо оптимизирован для перемещения неинкапсулированных баз данных, которые работают на других БД к нему. В частности, установка sql_mode с одной стороны облегчает перенос кода на MySQL, а с другой делает его относительно небезопасным для такого рода инкапсуляции.
Однако рыночные факторы не единственные, которые отталкивают разработчиков от создания баз данных таким образом. Постоянные дебаты по хранимым процедурам иллюстрируют несоответствие по крайней мере одного обычно используемого инструмента и типичного варианта использования этого инструмента.
Хранимые процедуры, как следует из названия, по сути являются императивными конструкциями, которые принимают конкретные аргументы и возвращают записи. Идея состоит в том, что они предоставляют базовый обязательный язык интерфейса для базы данных. Вместо SELECT …. вы вызываете CALL some_proc (arg1, ….);
Однако существует несколько проблем с хранимыми процедурами, поскольку они обычно используются. Во-первых, они все еще имеют существенное несоответствие с объектно-ориентированным программированием. Структуры данных, которые они возвращают, имеют тенденцию быть довольно жесткими, поэтому добавление нового столбца, как правило, требует многократных изменений в коде, часто, по крайней мере, по одному на каждом уровне программы. Вы не можете просто определить свой объект один раз, а затем снова и снова использовать его в других слоях вашего кода.
Вторая существенная проблема заключается в том, что хранимые процедуры лучше всего работают, когда они представляют собой несколько хорошо связанных и модульных запросов, и хуже всего, когда они представляют собой множество различных запросов, связанных между собой сложными способами. Это приводит к ограниченной полезности для фактической инкапсуляции данных, и во всех случаях уровень абстракции не совсем подходит для того, что обычно делается с ним. По этим причинам хранимые процедуры, которые обычно используются, имеют больше смысла при работе вне объектно-ориентированного подхода.
Хранимые процедуры были заменены инструментами отображения объектных отношений (ORM), которые пытаются обеспечить отображение между реляционным интерфейсом и объектно-ориентированной средой разработки. ORM автоматизируют базовые операции с базой данных для таких вещей, как операции вставки, выбора, обновления и удаления, но на самом деле они не обеспечивают абстракцию относительно фактического отображения данных между уровнем поведенческого приложения и уровнем информационной модели. В настоящее время это можно сделать только в самой информационной модели, поэтому ORM лучше всего сочетать с обновляемыми представлениями, но это происходит за счет переносимого кода SQL.
Помимо этих подходов или перехода на NoSQL, существует множество методов для инкапсуляции хранилища данных внутри реляционного приложения. Эти подходы требуют понимания как стремления к инкапсулатину и интерфейсам, так и желания интегрироваться с приложениями как сервисом, а не как относительно простым уровнем персистентности, управляемым математически.
Сервис-ориентированная архитектура базы данных
В течение большей части последних пяти лет я
создавал LedgerSMB, используя подход, который я называю «Сервис-ориентированная архитектура базы данных», или SODA, который частично основан на веб-сервисах RESTful и SOAP. В SOAP я сделал акцент на возможности обнаружения, а в REST — в максимально возможной степени — на цели повторного использования всего в базе данных, которое можно использовать повторно для определения API. Таким образом, этот подход использует семантику базы данных так, как REST повторно использует семантику HTTP, и хотя существуют некоторые различия, вызванные тем, как PostgreSQL работает (каждая функция, вызываемая оператором SELECT), это не конец света. Конечно, цель состоит в том, чтобы создать интерфейсы базы данных, подходящие для слабосвязанных комбинаций приложения / базы данных.
Подход SODA основан на ряде принципов, а именно:
- Доступ к базе данных осуществляется через функции, а не отношения,
- Функции, насколько это возможно, всегда возвращают полезный результат, обычно в структуре данных, соответствующей объекту,
- Имена функций (в домене этой архитектуры) уникальны, и
- Имена аргументов функции соответствуют ожидаемым свойствам.
- База данных отвечает за собственную безопасность.
Если они выполняются, функции могут быть сопоставлены, обнаружены и запущены во время выполнения. Вот класс PHP, который реализует такое отображение во время выполнения:
class DBObject { protected $schema = 'public'; /* function __call($procname, $order = null) * Maps in object properties into an arg array and calls call_procedure * * db procedures are checked for argument names and these are stripped of * the "in_" prefix. After this is complete, a property is matched and * mapped in. */ public function __call($procname, $order = null){ # variable initializations $procargs = array(); # proc data lookup $procquery = " SELECT proname, pronargs, proargnames, proargtypes FROM pg_proc WHERE proname = $1 AND pronamespace = coalesce((SELECT oid FROM pg_namespace WHERE nspname = $2), pronamespace)"; $db = DB::getObject(); $sth = pg_query_params($db->dbhandle, $procquery, array($procname, $this->schema)); $procdata = pg_fetch_assoc($sth); if (0 == pg_num_rows($sth)){ throw new \exception('Function not found'); } # building argument list preg_match('/^{(.*)}$/', $procdata['proargnames'], $matches); $procargnames = $phpArr = str_getcsv($matches[1]); foreach ($procargnames as $argname){ $argname = preg_replace('/^in_/', '', $argname); array_push($procargs, $this->$argname); } # calling call_procedure return $this->call_procedure($procname, $procargs, $order); } /* function call_procedure($procname, $args = array(), $order = null) * * generates a query in the form of: * select * from $procname($1, $2, etc) ORDER BY colname * and runs it. It returns an array of associative arrays representing the * output. */ public function call_procedure($procname, $args = array(), $order = null){ $results = array(); # query generation $query = "select * from " . pg_escape_identifier($this->schema) . "." . pg_escape_identifier($procname) . "("; $count = 1; $first = 1; foreach ($args as $arg){ if (!$first){ $query .= ", "; } $query .= '$' . $count; $first = 0; ++ $count; } $query .= ')'; if ($order){ $query .= " ORDER BY " . pg_escape_identifier($order); } # execution and returning results $db = DB::getObject(); $sth = pg_query_params($db->dbhandle, $query, $args); if (!$sth){ return null; } for ($i = 0; $i < pg_num_rows($sth); $i++){ print "retrieving row $i \n"; array_push($results, pg_fetch_assoc($sth, $i)); } return $results; } /* function merge(array $data) * merges data into the current object from an associative array * * null or undef values are not set */ public function merge($data){ foreach ($this as $prop => $value){ if (array_key_exists($prop, $data) and null != $data[$prop]){ $this->$prop = $data[$prop]; } } } /* function is_allowed_role($role) * returns true if the user is allowed the role for the specific db * i.e. $role should not include the prefix. Otherwise it returns false */ public function is_allowed_role($role){ $db = DB::getObject(); return $db->is_allowed_role($role); } }
Приведенный выше код кажется длинным, но он по существу позволяет наследовать объекты, чтобы просто объявить, что методы сопоставлены с хранимыми процедурами, и эти сопоставления автоматически корректируются во время фактического вызова хранимой процедуры. Кроме того, это централизует практически весь доступ к БД в одном файле, где его можно проверять на наличие проблем с внедрением SQL и т. Д., И вы можете продолжать программирование, как если бы вы работали с объектно-ориентированной базой данных. Конечно, бывают случаи, когда вам нужно вносить изменения во многие слои, например, когда необходимо добавить и сохранить новый атрибут, а его еще нет в таблице, но, как правило, это относительно редко.
В PHP у меня может быть класс, который проверяет версию и легко выбирает соответствующую хранимую процедуру, даже если они ожидают разные свойства объекта в качестве аргументов:
public function save(){ $procname = 'company__save'; if ('1.3' == \LedgerSMB\Config\DBVERSION){ $procname = 'company_save'; } $data = array_pop($this->$procname()); $this->merge($data); }
Как может выглядеть хранимая процедура? Вот один из них:
CREATE OR REPLACE FUNCTION asset_dep_straight_line_yr_d (in_asset_ids int[], in_report_date date, in_report_id int) RETURNS bool AS $$ INSERT INTO asset_report_line (asset_id, report_id, amount, department_id, warehouse_id) SELECT ai.id, $3, asset_dep__straight_line_base( ai.usable_life, -- years ai.usable_life - get_fractional_year(coalesce(max(report_date), start_depreciation, purchase_date), coalesce(start_depreciation, purchase_date)), get_fractional_year(coalesce(max(report_date), start_depreciation, purchase_date), $2), purchase_value - salvage_value, coalesce(sum(l.amount), 0)), ai.department_id, ai.location_id FROM asset_item ai LEFT JOIN asset_report_line l ON (l.asset_id = ai.id) LEFT JOIN asset_report r ON (l.report_id = r.id) WHERE ai.id = ANY($1) GROUP BY ai.id, ai.start_depreciation, ai.purchase_date, ai.purchase_value, ai.salvage_value, ai.department_id, ai.location_id, ai.usable_life; UPDATE asset_report SET report_class = 1 WHERE id = $3; select true; $$ language sql;
К сожалению, вышеприведенное должно возвращать true, потому что характер операции на самом деле не обеспечивает другого эффективного подхода, хотя, если мы найдем его, он будет скорректирован в следующем обновлении основной версии.
Такой подход, как правило, хорош, потому что он легкий и относительно хорошо соответствует более быстро меняющимся условиям. Однако отсутствие навязываемой структуры может быть проблемой и в некоторых средах. Там, где требуется больше техники, другие подходы могут работать лучше. Это работает относительно хорошо, однако, если вы строите свой API, чтобы предполагать относительно слабую связь между вашей базой данных и приложением, использующим этот вид API.
Объектно-ориентированный интерфейс уровня базы данных
Там, где требуется более тесная связь, объектно-ориентированный интерфейс может быть лучше. В некоторых случаях этого стоит избегать, потому что это приводит к очень уродливым SQL-запросам, например:
SELECT (save).* FROM save(row(null, '12345', 'My Company, LTD', 232, '33-55432334')::company);
Общая проблема здесь заключается в том, что у вас есть возможность обнаружения нескольких уровней. Это работает очень хорошо для генераторов кода, но не так хорошо или для людей-мастеров. Обратите внимание, что вышеперечисленное можно переписать, не допуская дополнительных аргументов:
SELECT (save).* FROM (row(null, '12345', 'My Company, LTD', 232, '33-55432334')::company).save;
Преимущество этого подхода состоит в том, что ваши объекты образуют классы, структура которых обнаружима, и становится возможной перегрузка. Таким образом, генераторы кода могут работать хорошо, поскольку база данных содержит всю информацию, необходимую для создания всего стандартного кода. Сам код базы данных также упрощен. С другой стороны, устранение неполадок может быть немного болезненным. Он также имеет свойство по существу требовать использования генераторов кода для создания библиотек для взаимодействия. Это тесно связывает сгенерированные библиотеки с созданным интерфейсом.
В базе данных ORM и JSON (в Javascript!)
Один захватывающий подход, с которым я недавно столкнулся, но с которым у меня очень мало опыта, это ORT
в базе данных xTuple, который в основном написан на хранимых процедурах pl / v8js. Да, вы правильно поняли, хранимые процедуры написаны на Javascript. Я хотел бы пригласить людей проверить код и посмотреть, что они думают. Это увлекательный подход, с которым я еще не играл, но он определенно показывает, насколько далеко может быть достигнута инкапсуляция в PostgreSQL.
Выводы
Инкапсуляция приложения в базе данных — это не то, что нужно делать в NoSQL. СУБД, которые являются сильно программируемыми, способны сделать это сейчас, хотя, возможно, немногие, если таковые имеются, конкурируют с гибкостью в этой области PostgreSQL. СУБД может тогда быть «сервером информационной модели», который обслуживает информационные модели по запросу, каждая из которых включает в себя дополнительные данные. Затем модель данных может быть использована и расширена в модели инфраструктуры MVC, но проблемы несоответствия импеданса могут быть в значительной степени устранены путем понимания и использования разделения интересов в свою пользу.
Конечно, все это не должно унижать NoSQL. Эти продукты довольно часто успешно используются в качестве дополнения к традиционным СУБД, как для предварительной обработки, так и для последующей обработки данных для последующего использования или повторного использования. Некоторые пользователи моделей хранения Polyglot обнаружили быстрое увеличение темпов разработки, когда они используются вместе, причем данные часто передаются из чего-то вроде Mongo или Neo4j в PostgreSQL после того, как они по существу используют эти механизмы для преобразования данных или используют его для преобразования данных в вывод. Это особенно верно при обработке больших объемов данных, которые должны обрабатываться относительно нереляционными способами, такими как графовые базы данных, массивы-базы данных (в науке) и т. Д. Сочетание этих подходов становится очень богатым.
Но это говорит о том, что дни использования СУБД в качестве тупого хранилища данных для данного приложения во многих случаях сочтены. Я не вижу ничего, что бросало бы вызов примату среды RDBMS, но в то же время это не означает никакой роли и для других. Если вам нужно тупое хранилище постоянных данных, вы можете использовать решение NoSQL и быть счастливее. Однако общий подход к предотвращению несоответствия путем инкапсуляции хранилища данных внутри приложения в равной степени применим к среде RDBMS, как и к любой другой.