A1: СУБД — Система Управления Базами Данных (DBMS — DataBase
Management System). Программа, либо комплекс программ, предназначенных для
полнофункциональной работы с данными. Как правило, включает в себя
инструменты для создания и изменения структуры хранения наборов данных, а
также средства доступа к хранимым данным, с возможностью их чтения,
добавления, изменения и удаления. При этом, у большинства СУБД имеется
собственный встроенный язык (возможно не один) для работы с данными. Сама
база данных (БД) обычно находится просто в файлах закрытого, либо открытого
формата.
A2: Под настольной (desktop) обычно подразумевается СУБД, которая
всегда запускается на компьютере пользователя, хотя сама база данных
может находиться в другом месте. В результате несколько копий СУБД могут
обращаться к одной базе данных. Серверная (server) СУБД, как правило,
запускается в на той же машине (сервере баз данных), где находятся файлы БД.
Непосредственно к базе данных обращается лишь один экземпляр СУБД.
Пользовательские приложения общаются только с этой СУБД через ее API, независимо
от того, работают они на той же машине или на другой. Для
многопользовательских баз данных более эффективным и надежным вариантом
является серверная СУБД. В ней гораздо быстрее происходит доступ к данным, и
значительно проще решаются конфликты между разными пользователями.
A3: Реляционная (relational) БД отличается способом представления
информации, находящейся в ней. Данные в такой базе хранятся в плоских
таблицах. Каждая таблица имеет собственный, заранее определенный набор
именованных колонок (полей). Поля таблицы обычно соответствуют атрибутам
сущностей, которые необходимо хранить в базе. Количество строк (записей) в
таблице неограниченно, и каждая запись соответствует отдельной сущности.
Каждая таблица должна иметь первичный ключ (ПК) — поле или набор
полей, содержимое которых однозначно определяет запись в таблице и
отличает ее от других. Связь между двумя таблицами обычно образуется
при добавлении в первую таблицу поля, содержащего значение первичного ключа
второй таблицы. Реляционные СУБД (РСУБД) предоставляют средства для
всевозможных пересечений и объединений любых таблиц, отбора
записей по разнообразным условиям, группировки и сортировки
результатов. Реляционная база данных сочетает наглядность представления
информации с простотой (относительной) реализации своей концепции и является
наиболее популярной структурой для хранения данных на сегодняшний день.
Достаточно подробно реляционные БД описаны в книгах [1], [2] и [3].
A4: Сервер для управления реляционными БД обычно называют
SQL-сервером. SQL (Structured Query Language — язык
структурированных запросов) является стандартным языком для работы с
реляционными БД. Кроме стандартных реляционных операций, этот язык предоставляет
возможности для изменений структуры таблиц. Различные варианты SQL
используются во всех, как серверных, так и в настольных реляционных СУБД.
A5: На какой хватит денег.:) Вообще-то это может сильно зависеть от
постановки задачи, количества пользователей и прихотей заказчика. Ниже
приведена таблица самых распространенных SQL-серверов в порядке
(примерно) убывания их возможностей:
A6: В объектно-ориентированных БД (ООБД), данные представлены в виде
объектов различных классов. Как правило, имеются возможности
создавать новые классы, наследовать их от уже имеющихся, задавать произвольные
атрибуты и методы для классов. Для доступа к объектам, в каждой ООБД обычно
предусматривается свой собственный язык, либо расширение другого языка. Пока еще
ООБД недостаточно развиты и не представляют серьезной конкуренции
SQL-серверам, хотя и выглядят более предпочтительными для разработчиков.
Производители SQL-серверов тоже, в свою очередь, иногда делают попытки
соорудить над реляционным ядром сервера объектно-ориентированную
надстройку. Более подробно ООБД описаны в статье [4].
A8: Пост-реляционными, часто называют многомерные базы данных.
Данные в многомерных базах, представляются в виде разреженных многомерных
массивов, а не плоских таблиц, как в реляционных базах. Для определенных
задач, многомерные базы могут давать значительный выигрыш в быстродействии, по
сравнению с реляционными. Наиболее известные многомерные СУБД: Cache Teradata
A9: Кроме реляционных, объектно-ориентированных и
многомерных СУБД, также давно известны иерархические и
сетевые базы данных. Данные и связи между ними, в
иерархических БД представлены в виде деревьев. Для некоторых задач, такая
форма представления данных может оказаться гораздо более эффективной, чем любая
другая. В сетевых базах, данные могут быть связаны произвольным
образом, но эти связи должны создаваться предварительно, вместе со структурой
данных. По сравнению с реляционными БД, сетевая модель может давать выигрыш в
быстродействии, при некоторой потере гибкости.
A1: Под сервером БД обычно подразумевается СУБД, запущенная на той же
машине, где находятся файлы БД, и монопольно распоряжающаяся этими
файлами. При этом, все пользовательские приложения должны работать с
базой только через эту СУБД, используя ее язык запросов.
A2: Клиентом к БД, обычно называют пользовательское приложение,
которое общается с сервером БД. Модель работы, в которой клиент общается
непосредственно с сервером, не используя промежуточных приложений,
называется архитектурой клиент-сервер.
A3: На пользовательских машинах, обычно устанавливаются специальные
программы-шлюзы, которые, через сетевой протокол, обеспечивают связь с
сервером БД. Через эти шлюзы, приложения передают запросы серверу и получают
результаты. Часто, дополнительно устанавливается библиотека (ODBC, OLE DB и
т.п.), предоставляющая приложениям API для работы с сервером БД.
A4: Довольно распространена модель работы, когда клиент обращается не
непосредственно к серверу БД, а к промежуточной программе. Эта программа
обычно называется сервером приложений. Такую архитектуру называют
«трехзвенной», в отличие от «двухзвенной» архитектуры
клиент-сервер.
A5: Сервер приложений может использоваться для многих целей. Как правило,
сервер приложений находится на отдельной машине. На него можно переложить
всю функциональность программы, оставив клиенту только интерфейсную
часть. Это разгрузит клиента и сервер БД от вычислений. Также, при большом
количестве пользователей, можно использовать несколько серверов приложений
для распределения нагрузки. А для ускорения доступа к часто
используемым таблицам, их обычно кэшируют на сервере приложений.
A6: «Тонким» клиентом обычно называют пользовательское приложение,
не содержащее никакой функциональности, и предназначенное только для
ввода/вывода информации. Вся обработка данных производится на сервере БД,
либо на сервере приложений. Зачастую, такой клиент изначально не содержит
вообще никаких возможностей, а подгружает дополнительные модули с
сервера, по мере необходимости. Обычно, в качестве «тонкого» клиента,
выступают Web броузер + HTML/ASP/Java. «Толстый» клиент содержит всю
функциональность и интерфейсную часть в себе, и при любом изменении, требует
замены у всех пользователей.
A1: Данные в реляционной БД хранятся в плоских таблицах. Каждая
таблица имеет собственный, заранее определенный набор именованных колонок
(полей). Поля таблицы обычно соответствуют атрибутам сущностей, которые
необходимо хранить в базе. Количество строк (записей) в таблице неограниченно, и
каждая запись соответствует отдельной сущности. Полное описание реляционных
БД можно найти в книгах [1], [2] и [3].
A2: Записи в таблице отличаются только содержимым их полей. Две записи, в
которых все поля одинаковы, считаются идентичными. Каждая
таблица должна иметь первичный ключ (ПК) — поле или набор полей,
содержимое которых однозначно определяет запись в таблице и отличает ее
от других. Отсутствие первичного ключа и наличие идентичных записей в
таблице обычно возможно, но крайне нежелательно.
A3: Простейшая связь между двумя таблицами образуется при добавлении в
первую таблицу поля, содержащего значение первичного ключа второй таблицы.
В общем случае, реляционные БД предоставляют очень гибкий
механизм для всевозможных пересечений и объединений любых таблиц, с
разнообразными условиями. Для описания множеств, получающихся при
пересечении и объединении таблиц, используется специальный математический
аппарат — реляционная алгебра.
A4: Упорядочивание модели БД. Грубо говоря, нормализацией называют
процесс выявления отдельных независимых сущностей и вынесения их в
отдельные таблицы. При этом, связи с такими таблицами, обычно организуют по их
первичному ключу. В результате нормализации, увеличивается гибкость
работы с БД. Также, уменьшается содержание дублирующей информации в БД, а
это сильно понижает вероятность возникновения ошибок.
A5: Нет. Реляционная алгебра оперирует множествами, в
которых порядковый номер элемента не несет никакой смысловой нагрузки. Записи
отличатся только содержимым их полей. Две записи, в которых все поля
одинаковы, будут абсолютно идентичны в реляционной БД.
A1: SQL (Structured Query Language — язык структурированных
запросов) является стандартным языком для работы с реляционными БД.
Разделяется на две основные части: DDL (Data Definition
Language — язык определения данных) и DML (Data Manipulation
Language — язык обработки данных). DDL предоставляет средства
для создания и изменения структуры хранения данных (БД, таблиц, процедур,
типов данных и т.п.). DML предназначен для чтения и изменения
данных. Основные операторы DML: select — выборка,
insert — вставка, update — изменение,
delete — удаление. Также, с помощью SQL, часто реализован
доступ к служебным функциям SQL-сервера (заведение пользователей, создание
резервных копий БД и т.д.). Подробное описание операторов и возможностей SQL
имеется в книге [1].
A2: Во многих случаях, необходимо проведение группы операций по изменению
данных таким образом, чтобы эта группа обладала свойством атомарности
(либо вся целиком выполняется, либо вся целиком не выполняется). Такая
группа операций называется транзакцией. В SQL-серверах существуют
операторы, позволяющие обозначить начало транзакции (begin transaction),
ее успешное завершение (commit transaction), либо откат транзакции
(rollback transaction).
A3: Любые изменения данных, проведенные внутри транзакции, записываются в
специальный журнал транзакций (transaction log). При откате
транзакции, данные восстанавливаются в прежнем виде, а записи об изменениях
удаляются из журнала транзакций.
A4: При изменении данных внутри транзакции, модифицируемые записи
блокируются сервером до окончания этой транзакции. Если какая-нибудь
другая транзакция пытается изменить заблокированные записи, то ее выполнение
останавливается, пока не будет снята блокировка, то есть, пока не
завершится первая транзакция. Некоторые сервера имеют неприятную особенность
блокировать данные не отдельными записями, а целыми страницами, которые
могут содержать довольно много записей.
A5: Классические «блокировочники» не дают возможности разным
транзакциям одновременно изменять одни и те же записи, блокируя их на время
транзакции. В результате, при попытке изменить заблокированную запись,
другая транзакция будет простаивать, пока не завершиться первая. В свою
очередь, «версионники» позволяют одновременно модифицировать одни и те же
записи, создавая при этом разные версии одной записи.
A6: Перекрестная блокировка (deadlock) двух транзакций возникает при
изменении одних и тех же записей в разном порядке. Последовательность
действий, приводящая к перекрестной блокировке: 1. Транзакция A изменяет
запись X. Заблокирована X. 2. Транзакция B изменяет запись Y. Заблокирована
Y. 3. Транзакция A пытается изменить запись Y. Остановлена A. 4.
Транзакция B пытается изменить запись X. Остановлена B. Сервер определяет
перекрестную блокировку и откатывает одну из транзакций, возвращая ошибку
соответствующему соединению. Аминь. Чтобы не выводить ошибку пользователю,
обломанное соединение должно молча повторить транзакцию.
A7: Для ускорения операций выборки данных. При поиске полей с
определенным значением, сервер вынужден перебирать все записи в таблице. В этом
случае, время поиска линейно зависит от размера таблицы. Индекс по
полю, обычно представляющий собой бинарное дерево, дает возможность резко
сократить время поиска, превратив эту зависимость в
логарифмическую. Однако, наличие индексов в таблице, замедляет
операции модификации данных.
A8: Первичный ключ (ПК) — поле или набор полей, содержимое
которых однозначно определяет запись в таблице и отличает ее от других.
Служит для однозначной идентификации записей и в таблице может быть только один.
Обычно, при определении первичного ключа, по нему автоматически создается
уникальный индекс.
A9: Триггер — процедура, выполняемая сервером
автоматически при модификации данных в таблице. В основном, триггеры
используются для поддержания целостности дублирующей информации в
денормализованной БД.
A10: Можно, практически во всех современных SQL-серверах. Различия только в
синтаксисе определения и вызова функции. Кроме того, некоторые сервера
позволяют использовать функции, написанные на других языках (не SQL).
A11: Обычно, в SQL-серверах можно указать одно из числовых полей как
автоматически нумеруемое, причем синтаксис определения сильно зависит от
разновидности сервера. При вставке новой записи, такому полю присваивается
значение поля предыдущей записи + заданное приращение. Как правило,
значение подобного поля нельзя явно изменять с помощью DML.
A12: Оптимизатор предназначен для определения самого быстрого
способа выполнения запроса (в основном для выборки данных), при участии в
нем нескольких таблиц. В простейшем случае, задача оптимизатора заключается
в правильном использовании индексов при соединении таблиц и применении
дополнительных условий. Иногда, можно задать в запросе дополнительную информацию
(hint) для оптимизатора.
A13: Представление (view) — это запрос на выборку,
хранящийся на сервере, как отдельный объект. Так как, результат этого
запроса можно рассматривать в качестве таблицы, представление допускается
использовать в других запросах, также как любую обычную таблицу.
Материализованное представление хранится на сервере в виде
таблицы, которая автоматически обновляется при изменении данных,
имеющих отношение к этому представлению.
A14: Хранимые процедуры (SP — Stored Procedure) представляют
собой последовательность команд на расширениях SQL, либо на других языках,
поддерживаемых сервером. Могут принимать параметры и возвращать значение
заданного типа. Часто используются для выполнения операций, напрямую
связанных с логикой задачи, для которой проектировалась БД. Иногда,
используются вместе с представлениями, для обеспечения безопасности БД
(все изменения через SP, все выборки через view).
A15: Обычно, для полей в таблицах могут использоваться только самые простые
типы: числа (целые и дробные), строки (сильно ограниченные по
длине), дата (и время), бинарные данные большого размера (для
текста, графики и т.п.). В некоторых серверах допускается использование
массивов и самодельных структур.
A16: А хрен его знает. И это совсем не шутка, а его фактическое
определение. Null не является пустым значением. Поэтому null +
1 = null, а не 1. Использование null означает переход от двузначной
логики (true/false) к трехзначной (true/false/unknown), которая
является более сложной и несколько непривычной. С другой
стороны, логические операторы SQL обычно приспособлены для работы только с
двузначной логикой. Поэтому использовать null для значений полей следует очень
осторожно и только в тех случаях, когда без него никак нельзя обойтись.
Поведение null хорошо описано в статье [5].
A17: Внешний ключ (FK — Foreign Key) используется для создания
жесткой связи (многие к одному) между двумя таблицами. Внешний ключ
задается только в том случае, если в первой таблице есть поле, содержащее
значение первичного ключа из второй таблицы. При изменении
значения первичного ключа во второй таблице, могут быть изменены все
соответствующие значения связанного поля в первой таблице. При
удалении записи с определенным первичным ключом из второй таблице, могут
быть удалены все записи с соответствующим значением связанного поля в
первой таблице. Обычно, при определении внешнего ключа, по нему
автоматически создается индекс, который используется в запросах при объединении
этих двух таблиц.
A18: Вообще-то он есть (ANSI SQL 92), только вот поддержка его у
производителей СУБД сильно хромает. Причем дело с эти настолько
плохо, что может различаться даже синтаксис основных операторов DML,
не говоря уже о функциях, триггерах и прочих особенностях SQL. Поэтому о
совместимости говорить сложно, хотя кое-какие сдвиги в этом направлении имеются.
Наиболее близки к стандарту: Borland InterBase, Microsoft SQL Server, IBM
DB2 Universal Database.
A19: Наиболее распространенным клиентским API является ODBC
(Open DataBase Connectivity). ODBC-драйвера существуют на многих платформах
и почти для всех SQL-серверов.
A20: Репликацией обычно называют процесс синхронизации данных
между несколькими БД. Наиболее развитые SQL-сервера содержат
встроенные средства репликации. Для остальных могут быть использованы
продукты сторонних фирм. Одностороняя репликация подразумевает
изменение данных только в одной базе, с последующей передачей изменений на
остальные. Соответственно, довольно проста в реализации и надежна
в работе. Двустороняя репликация предоставляет гораздо более мощный
инструмент распределенной работы между SQL-серверами. Плата за это —
сложность и большая вероятность конфликтов при работе.
A1: Естественным ключом (ЕК) обычно называют первичный ключ, который
является смысловым атрибутом (либо набором атрибутов) сущности. Как и
любой другой атрибут, ЕК подвержен изменениям, а его уникальность часто
бывает притянута за уши. Суррогатным ключом (СК), как правило,
называется уникальное автонумеруемое целочисленное поле в таблице,
которое и является первичным ключом. СК гораздо компактнее, чем ЕК, везде
занимает одно поле одинакового типа, всегда неизменен и уникален.
Более обоснованное сравнение СК и ЕК производится в статье [6].
A2: Только в теории. Полностью нормализованная БД хороша для
хранения данных. Но, на практике постоянно встречаются структуры,
получение информации из которых, слишком сложно и медленно (а
иногда и невозможно с помощью лишь оператора select). Дублирование
информации в том или ином виде обычно необходимо для получения результатов
выборки простыми запросами за приемлемое время.
A3: А не надо их удалять, тогда и проблемы не будет. Проще всего
держать в таблице дополнительное поле Deleted (default = false), а вместо
удаления записи устанавливать Deleted = true. Для выборки можно использовать
представление с внутреннем условием:
Deleted = false
Возможен и более сложный вариант: два дополнительных поля DateBegin
(defaul = текущая_дата) и DateEnd (defaul = максимальная_дата). Вместо
удаления записи необходимо установить DateEnd = текущая_дата, а условие для
выборки будет:
текущая_дата beetwen DateBegin and DateEnd
Во втором варианте есть возможность, не только иметь даты
заведения/удаления записей, но и получать наборы записей на любую
определенную дату с условием:
A4: Самый простой вариант — это создать таблицу-журнал, в
которую, при изменении записи, помещать следующую информацию: дату
изменения, имя (Id) пользователя, имя (Id) изменяемой таблицы, ПК записи, имя
(Id) изменяемого поля и предыдущее значение поля, преобразованное в
строку. Запись в журнал можно делать либо триггерами, либо из
приложения. Откат изменений следует производить специальной
процедурой, которая должна восстановить значения полей на заданную дату и
удалить соответствующие записи из журнала. Один из вариантов журнала
имеется в статье [9].
A5: Можно использовать сервер, который поддерживает историческую
модель, например, PostgreSQL Server. В ином случае, можно, в дополнение
к основной таблице, создать аналогичную таблицу истории, с двумя
дополнительными полями: DateBegin и DateEnd. При
вставке записи в основную таблицу, необходимо добавить такую же запись в
таблицу истории, где DateBegin = текущая_дата, DateEnd = максимальная_дата.
При модификации записи в основной таблице, нужно вставить измененную
запись в таблицу истории, где DateBegin = текущая_дата, DateEnd =
максимальная_дата. При этом у предыдущей соответствующей записи в таблице
истории необходимо установить DateEnd = текущая_дата. При удалении
записи в основной таблице, у последней соответствующей записи в таблице истории
надо установить DateEnd = текущая_дата. Выборка из основной таблицы
производится обычным образом, а из таблицы истории с условием:
заданная_дата beetwen DateBegin and DateEnd
Сходные варианты представления истории рассматриваются в статье [10].
A6: В таблицу, которую надо сделать иерархической, надо добавить поле
Parent, содержащее значение ПК родительской записи. Далее, все
зависит от задачи и возможностей сервера. Если нет необходимости
углубляться в иерархию более чем на один уровень, либо сервер поддерживает
рекурсивные запросы, то этого вполне хватит. Иначе, придется использовать
дополнительные поля, либо вспомогательную таблицу. Подробно
способы представления деревьев в реляционных БД описаны в статьях [7] и [8].
A7: Вообще-то, в большинстве SQL-серверов есть системный
каталог — набор стандартных таблиц, содержащих информацию обо всех
объектах сервера и БД. Правда, таблицы эти предназначены не для
разработчика, а для внутреннего использования самим сервером, поэтому
найти там что-либо весьма проблематично. При этом, структура их может меняться
даже в разных версиях одного сервера. Поэтому, желательно создать в БД
собственный системный каталог, полностью описывающий структуру базы и
взаимосвязи ее объектов. Такой каталог можно переносить на другой сервер
вместе с БД, не заботясь о совместимости, а информацию из него можно легко
использовать в приложении для каких-либо действий (вплоть до
автоматической генерации самого приложения). Пример системного каталога
приведен в статье [9].
A8: Проще всего файлы держать отдельно в выделенной директории на
сервере, а в базе хранить только их имена. Единственной проблемой в таком случае
будет раздача прав на эту директорию. Кроме того, в SQL-серверах есть
возможность определять бинарные поля большого размера, которые можно
использовать для хранения разных видов файлов. Чтобы не замедлять работу с
обычными данными, рекомендуется создавать отдельно таблицу, содержащую такие
поля. Также, при использовании больших полей, можно обнаружить разнообразные
ограничения на сервере и клиенте.
A9: Такие действия проще всего выполнять на клиенте. Если надо
на сервере, то лучше использовать предназначенные для этого возможности
оператора выборки:
1. select top n * from t order by a
2. select * from t order by a limit n,m
Когда ничего такого сервер не поддерживает, можно использовать в условии
выборки подзапрос, возвращающий номер текущей строки результата (если
поле сортировки не уникально, то в подзапрос нужно добавить еще какое-нибудь
условие для обеспечения уникальности значений, возвращаемых подзапросом):
select * from t as t_outer where
( select count( * ) from t as t_inner where t_inner.a <= t_outer.a ) between n and m
order by a
В крайнем случае, можно выбирать данные во временную таблицу с
дополнительным автонумеруемым полем RowNumber, а из нее затем делать
выборку:
select ... where RowNumber >= n and RowNumber <= m
A10: Самый правильный способ — это использовать вложенный
запрос, в котором ищется максимальное значение поля. Затем найденное
значение используется в условии основного запроса.
select *
from t
where a = ( select max( a ) from t )
Если максимальное значение необходимо искать не во всей таблице, а по
определенному полю, то запрос будет следующего вида:
select *
from t as t_outer
where a = ( select max( t_inner.a ) from t as t_inner where t_inner.b = t_outer.b )
Чтобы избежать появления в результате запроса нескольких одинаковых
записей, соответствующих одному максимальному значению, можно использовать
ключевое слово distinct.
A11: Подобные запросы обычно умеют делать разнообразные генераторы
отчетов. SQL же не предусматривает такой возможности. Хотя, при желании,
можно сделать процедуру, использующую динамический SQL. В ней придется
составлять запрос на основе данных из таблицы, а затем выполнять
его.
A12: Нумерация записей очень легко делается на любом клиенте.
Чтобы в запросе получить поле, соответствующее номеру записи, можно
использовать подзапрос, возвращающий количество записей, меньших
либо равных (сравнение — по полю сортировки) текущей записи:
select *,
( select count( * ) from t as t_inner where t_inner.a <= t_outer.a ) as RowNumber
from t as t_outer
order by a
Если поле сортировки не уникально, то в подзапрос необходимо добавить
дополнительное условие сравнения (например, по первичному ключу) для
обеспечения уникальности значений, возвращаемых подзапросом.
A13: Можно вообще хранить все дополнительные объекты в одной таблице,
содержащей идентификатор объекта, идентификатор атрибута и само значение
атрибута, преобразованное в строку. Чтобы не было мучительно больно при
получении результатов запросов, лучше все-таки использовать несколько
таблиц — отдельно для каждого типа данных. Более серьезный
вариант универсального хранилища объектов рассмотрен в статье [11]. PS: А чем
была плоха идея насчет создания/изменения таблиц?
API — Application Program Interface DB — DataBase
DBMS — DataBase Management System DDL — Data
Definition Language DML — Data Manipulation Language
FK — Foreign Key ODBC — Open DataBase
Connectivity PK — Primary Key RDBMS — Relational
DataBase Management System SP — Stored Procedure
SQL — Structured Query Language
Русские
БД — База Данных ЕК — Естественный Ключ
ООБД — Объектно-Ориентированная База Данных
ООСУБД — Объектно-Ориентированная Система Управления Базами
Данных ПК — Первичный Ключ РСУБД — Реляционная
Система Управления Базами Данных СУБД — Система Управления
Базами Данных СК — Суррогатный Ключ
This is a collection of questions and answers about relational databases, including SQL syntax, data modeling, normalization, indexing, and various database systems such as PostgreSQL, MySQL, and Oracle.
The questions cover a wide range of topics, includ
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.