Программирование баз данных в Delphi

Транзакции

Показывать лекцию целиком

На протяжении всего курса нам не раз приходилось сталкиваться с этим термином. Тема " транзакций " в InterBase непростая, но очень необходимая для понимания. Эта лекция посвящена теории транзакций, и практике их применений в приложениях.

В лекции № 14 мы упоминали, что транзакции - это пакет запросов, который последовательно производит изменения БД и либо принимается, если все изменения записи подтверждены, либо отвергается, если хоть один запрос завершился неуспешно. Запросы могут состоять из операторов SELECT / INSERT / UPDATE / DELETE, причем в контексте одной транзакции может быть как один такой запрос, так и множество запросов. Однако понятие " транзакция " гораздо глубже этого короткого определения.

Транзакции запускаются на стороне сервера, причем стартуют они только по приказу клиентского приложения. Завершают работу они также по приказу клиентского приложения, причем при успешном завершении транзакция подтверждается, а при неудаче - отвергается. В триггерах или процедурах вызвать старт транзакции невозможно.

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

Поясним эту суть на классическом примере перевода денег в банке с одного счета на другой.

Допустим, наша БД работает без транзакций, и нам нужно произвести упомянутый перевод. Тут мы можем поступить двумя разными способами:

  • Вначале снимаем деньги с одного счета, затем добавляем их к другому счету.
  • Вначале добавляем деньги к другому счету, затем снимаем их с первого счета.
  • Теперь предположим, что в середине этой операции произошел какой-то сбой: отключился сервер БД, например. В первом случае деньги будут потеряны - они ушли с одного счета, но не дошли до другого. Во втором случае деньги "размножатся" - появятся на втором счету, но при этом останутся и на первом. И в том, и в другом случае произойдет нарушение целостности БД - данные станут недостоверны.

    Однако все SQL -серверы баз данных работают с применением транзакций. Еще говорят, что все изменения базы данных происходят в контексте одной или нескольких транзакций. InterBase не исключение, более того, InterBase предоставляет гораздо более гибкие инструменты для управления транзакциями, чем многие другие SQL -серверы. Если произошел какой то сбой при переводе денег, то транзакция не получила подтверждения, а база данных осталась в прежнем состоянии - целостность и достоверность БД не нарушились.

    Более 20 лет назад исследователи Тео Хендер и Андреас Рютер опубликовали обзор, в котором описывали принципы поддержания целостности БД в много-клиентской среде. Эти принципы принято называть ACID ( Atomicity, Consistency , Isolation, Durability - Атомарность, Согласованность, Изоляция и Устойчивость ). Все транзакции действуют по этим принципам.

    Атомарность (Atomicity)

    Атомарность подразумевает, что транзакция является единицей работы с базой данных. Внутри транзакции может происходить множество модификаций БД, однако транзакция действует по принципу "все или ничего". Когда транзакция подтверждается ( Commit ), то подтверждаются все изменения данных, сделанные в ее контексте. Когда она отвергается (откатывается, Rollback ), то отвергаются и все изменения. В случае возникновения сбоя, система, восстанавливаясь, ликвидирует последствия транзакций, не успевших завершиться.

    Согласованность (Consistency)

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

    На стороне сервера за согласованность отвечают ограничения CHECK, ограничения ссылочной целостности и триггеры. Программист, тем не менее, должен тщательно спроектировать механизмы бизнес-логики.

    Изолированность (Isolation)

    В базе данных может выполняться множество транзакций одновременно. Бывает, что две, и более транзакции пытаются изменить одну и ту же запись. Чтобы гарантировать целостность данных, транзакции выполняются изолированно друг от друга. Можно сказать, что каждая транзакция работает со своей копией (версией) данных. Существует несколько степеней изолированности транзакций, о чем далее мы поговорим подробней.

    Устойчивость (Durability)

    Если транзакция завершается успешно, изменения, сделанные в ее контексте, должны быть устойчивыми и сохранятся, независимо от ошибок в других транзакциях, ошибок оборудования или аварийного завершения работы сервера. Другими словами, результаты работы успешно завершенной транзакции физически сохраняются в базе данных.

    Неявный и явный старт транзакций

    Все действия над базой данных, совершаемые в клиентском приложении, происходят внутри (в контексте) транзакции. В примерах предыдущих лекций мы соединяли клиентские приложения с базой данных, вообще не используя никаких транзакций. Однако это не значит, что их не было. Просто транзакции запускались неявно, автоматически. Причем с параметрами, созданными Delphi "по умолчанию". В серьезных приложениях БД это недопустимо, так как может привести к многочисленным конфликтам.

    Транзакцию можно стартовать и явно. Из стандартных механизмов доступа мы будем использовать, в основном, InterBase Express (IBX). В приложении должен присутствовать как минимум, один компонент IBTransaction. С помощью этого компонента можно явно указать параметры транзакции, управлять стартом, подтверждением или откатом транзакции. Делается это с помощью следующих методов компонента:

  • StartTransaction - Старт транзакции.
  • Commit - Подтверждение транзакции с последующим ее закрытием.
  • CommitRetaining - Подтверждение транзакции без ее закрытия.
  • Rollback - Откат транзакции с последующим ее закрытием.
  • RollbackRetaining - Откат транзакции без ее закрытия.
  • Впрочем, компоненты доступа к данным неявно производят запуск транзакции, поэтому StartTransaction обычно пропускают. А вот подтверждение или откат транзакции проверяют, как правило, в блоке try…except в клиентском приложении:

    try
       //какие то действия над данными...
       IBTransaction1.Commit; //подтверждаем
    Except
       //произошла ошибка
       ShowMessage('Невозможно выполнить операцию!');
       IBTransaction1.Rollback;  //делаем откат транзакции
    end; //try

    В данном примере, если транзакция прошла успешно, то данные нормально сохраняются. В случае какой-то ошибки выводится сообщение, и транзакция откатывается.

    Как транзакция работает

    В базе данных имеется специальная область, которая называется TIP ( Transaction Inventory Page - Инвентарная Страница Транзакций). При старте транзакции, ей присваивается идентификатор ( TID, Transaction ID ) - инвентарный номер, который сохраняется в TIP. При этом у самой последней транзакции будет наибольший идентификатор. В TIP, помимо номера стартовавшей транзакции, сохраняется и ее состояние, которое может быть Active (В работе), Committed (Подтвержденная), Rolled Back (Отмененная, откат) и In Limbo (Неопределенная).

    Активной называется транзакция, которая в настоящий момент выполняется.

    Подтвержденной называется транзакция, которая успешно завершила свою работу, как правило, по команде Commit.

    Отмененной называют транзакцию, которая завершилась неудачно. При этом производится откат сделанных ей действий, как правило, командой RollBack.

    Неопределенной транзакцией ( Limbo ) называют транзакцию, которая работает одновременно с двумя или более базами данных. При завершении такой транзакции, InterBase совершает двухфазное подтверждение Commit, гарантируя, что изменения будут внесены либо во все БД, либо ни в одну. При этом подтверждения в базах данных будут даваться по очереди. Если в это время возникнет сбой системы, то может получиться, что в каких то БД изменения были сделаны, а в каких то нет. При этом транзакция переходит в неопределенное состояние, когда сервер не знает, следует ли подтвердить эту транзакцию, или откатить.

    Каждая транзакция, начиная работу, создает собственную версию записей таблиц, с которыми работает. Версия записи - это копия записи, которая создается, когда транзакция пытается ее изменить. Таким образом, каждая запись таблицы потенциально может иметь множество версий, при этом каждая транзакция работает с собственной версией этой записи. Если транзакция меняет данные, то она меняет их в собственной версии записи, а не в оригинале. Далее транзакция может либо подтвердиться, либо отмениться.

    Если транзакция подтвердилась, InterBase попытается пометить предыдущую оригинальную запись, как удаленную, а версию завершенной транзакции - как оригинал. Когда InterBase сохраняет изменения, то в новую запись помещается и идентификатор транзакции, которая внесла эти изменения (любая строка таблицы содержит идентификатор создавшей ее транзакции ).

    Если транзакция завершилась неуспешно, оригинал записи так и остается оригиналом.

    Если же транзакция только читает запись, не пытаясь ее изменить, то для нее не создается собственной версии.

    Однако могут возникать и конфликты транзакций. Предположим, что стартовала транзакция Т1. Она создала версию записи, и поменяла ее данные. В это время стартовала конкурирующая транзакция Т2, и создала версию той же записи. Поскольку Т1 еще не завершилась, Т2 при старте не могла видеть изменения данных, сделанные Т1, а значит, создала свою версию из старого оригинала. Теперь Т1 завершает работу по Commit. Как должен поступить InterBase? Если он пометит версию записи Т1 как оригинал, а старую запись, как удаленную, то в версии Т2 окажутся ложные данные! Действия InterBase в этом случае будут зависеть от параметров этих транзакций, о чем ниже мы поговорим подробней.

    Если транзакция удаляет какую-то строку, то строка физически не удаляется из базы данных, а лишь помечается, как удаленная, сохраняя и номер удалившей ее транзакции. В случае если транзакция завершена неудачно, реального удаления строки не происходит, так как не было подтверждения.

    Таким образом, говорят, что InterBase имеет многоверсионную архитектуру ( MGA - Multi Generation Architecture ). Такая архитектура позволяет организовать работу с базой данных так, чтобы читающие пользователи не блокировали пишущих. Кроме того, при возникновении сбоев в системе, InterBase очень быстро восстанавливается, благодаря именно MGA. Кстати, InterBase является первым SQL -сервером, который поддерживает многоверсионную архитектуру.

    Наряду с преимуществами такого подхода, в базе данных со временем накапливается "мусор". Каждая транзакция, пытающаяся изменить данные, создает собственные версии строк, и если не позаботиться о своевременном удалении старых, уже никому не нужных версий, то база данных вскоре будет просто забита мусором.

    Но как удалять мусор? Можно ли удалить версию транзакции, которая завершилась, удачно или неудачно? Нет, если эта версия в настоящий момент используется другими транзакциями. Позже мы поговорим об уровнях изолированности транзакций, пока лишь скажем, что некоторые транзакции могут видеть изменения, сделанные другими, еще не подтвержденными активными транзакциями.

    Предположим, что стартовала транзакция Т1. Эта транзакция создала версию записи и модифицировала ее. Позже стартовала транзакция Т2, которая настроена так, чтобы видеть все изменения данных, даже не подтвержденные. Она обратилась к той же записи, а поскольку она желает видеть последние изменения, ей предоставляют версию транзакции Т1. Затем Т1 завершила свою работу, но Т2 пока еще работает с ее версией записи, следовательно, эту версию удалять нельзя.

    В InterBase присутствует механизм удаления старых версий, который запускается новыми транзакциями. Новая транзакция, запрашивая запись, считывает все версии этой записи. При этом делается проверка на то, была ли транзакция, сделавшая эту версию, отменена ( RollBack ) или подтверждена ( Commit ). Если транзакция была отменена, значит, эта версия - мусор, который следует удалить. Если же имеется несколько версий, сделанных подтвержденными транзакциями, то актуальной считается версия с наибольшим идентификатором транзакций. Остальные версии считаются устаревшими и также подлежат удалению.

    Таким образом, молодые транзакции прибирают базу данных от мусора, оставленного более старыми транзакциями. Но чистят они не все старые версии подряд, а только версии той записи (или записей), к которой обращаются сами.

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

  • Активная транзакция - транзакция, которая стартовала, но еще не завершена.
  • Заинтересованная транзакция - это транзакция, конкурирующая с текущей транзакцией.
  • Старейшая активная транзакция - это такая активная транзакция, которая стартовала раньше других. Или иначе, это активная транзакция с наименьшим идентификатором.
  • Старейшая заинтересованная транзакция - это такая заинтересованная транзакция, которая стартовала раньше других. Или иначе, это заинтересованная транзакция с наименьшим идентификатором.
  • В данном контексте, сборкой мусора занимается старейшая активная транзакция. Так как версий записи, сделанных более молодыми транзакциями, она видеть не может, то убирает мусор, оставленный еще более старыми транзакциями. Когда эта транзакция заканчивает свою работу, то статус "старейшей активной" переходит к другой транзакции. Таким образом, транзакции передают друг другу обязанность по сборке мусора.

    Уровни изолированности транзакций

    Как уже упоминалось выше, каждая транзакция работает изолированно от других транзакций. Уровень изолированности транзакции определяет, какие изменения, сделанные другими транзакциями , увидит данная транзакция

    InterBase имеет три уровня изолированности:

    READ COMMITTED - Читать подтвержденные данные. Этот уровень изоляции позволяет транзакции видеть изменения, сделанные другими, транзакциями, если эти изменения были подтверждены. К примеру, стартовали транзакции Т1 и Т2. Обе они изменили запись. Однако Т1 не увидит изменений, сделанных Т2, пока та не подтвердит сделанных ей изменений (по Commit ). Этот уровень изоляции считается самым низким, открытым, позволяя транзакции видеть "самые свежие" данные.

    SNAPSHOT - Моментальный снимок данных. Это средний уровень изолированности. Он позволяет транзакции видеть только те данные, которые существовали в момент старта транзакции. Если другие заинтересованные транзакции изменили запись и подтвердили изменения, то SNAPSHOT - транзакция все равно не увидит этих изменений. Однако она не блокирует данные, с которыми работает.

    SNAPSHOT TABLE STABILITY - еще более закрытый уровень изоляции. Транзакции такого уровня не только делают снимок данных, они также блокируют на запись те данные, с которыми работают. Пока такая транзакция не подтверждена, остальные транзакции гарантированно не смогут изменить эти данные. Кроме того, транзакция SNAPSHOT TABLE STABILITY просто не сможет получить доступ к таблице, если в настоящий момент другая транзакция изменяет в ней данные.

    Параметры транзакций

    Мы имеем возможность гибко управлять параметрами транзакций, добиваясь наилучших результатов. Параметры, установленные по умолчанию, далеко не всегда являются самыми удачными. Рассмотрим эти параметры.

    Возможные параметры транзакций
    Параметр Описание
    Read Разрешается чтение данных.
    Write Разрешается запись данных.
    Wait При возникновении конфликта с другой транзакцией, текущая транзакция ожидает определенное время (по умолчанию, 10 сек.), прежде чем попытается решить этот конфликт.
    NoWait При возникновении конфликта с другой транзакцией сразу генерируется ошибка.
    Read_committed Rec_Version Read _committed позволяет читать подтвержденные изменения данных, сделанные другими транзакциями. Дополнительный параметр Rec_Version, кроме того, позволяет читать и неподтвержденные изменения.
    Read_committed No_Rec_Vesion Read _committed позволяет читать подтвержденные изменения данных, сделанные другими транзакциями. Дополнительный параметр No_Rec_Version используется по умолчанию, и не позволяет читать неподтвержденные изменения.
    Concurrency Создает уровень изоляции SNAPSHOT.
    Consistency Создает уровень изоляции SNAPSHOT TABLE STABILITY

    Программист может задать транзакции несколько параметров одновременно. Например, если нужно, чтобы транзакция могла и читать, и изменять данные, можно задать параметры

    Read
    Write

    Какие параметры устанавливать, решается для каждой конкретной задачи. Однако можно сделать такие рекомендации:

    Если транзакция используется только для чтения, например, для вывода данных в сетку запросом SELECT, то она должна иметь параметры, позволяющие только читать данные, что значительно ускорит работу транзакции. Причем данные должны быть самыми "свежими", даже из неподтвержденных транзакций. И, кроме того, транзакция не должна ожидать разрешения возможного конфликта. Набор параметров тут может быть следующим:

    Read
    Read_Committed
    Rec_Version
    NoWait

    Если транзакция используется для построения отчетов, то она также должна только читать данные, но только подтвержденные. При этом желательно сделать снимок данных, чтобы не зависеть от возможных изменений. Для подобной транзакции параметры удобней всего делать такими:

    Read
    Concurrency
    NoWait

    И, наконец, если транзакция предназначена для изменения данных в БД, то ей лучше задать более жесткие параметры, например:

    Write
    Concurrency
    NoWait

    Практика применения транзакций

    Для закрепления материала создадим приложение, работающее с таблицей Tovar нашей базы данных. Приложение будет использовать две транзакции с различными параметрами - одну для чтения данных, другую для записи.

    Прежде всего, убедитесь, что сервер InterBase запущен. Далее, создайте в Delphi новый проект. На главную форму поместите DBNavigator, сетку DBGrid и простую кнопку:

    (рис 24.1) Главная форма приложения

    Пока мы не подключим сетку и навигатор к таблице, данные не будут отображаться. Сохраните проект в отдельную папку под именем IBXTrans. Поскольку и навигатор, и сетка нам нужны только для чтения данных, выделите навигатор, откройте сложное свойство VisibleButtons и сделайте невидимыми все кнопки, кроме nbFirst, nbPrior, nbNext, nbLast и nbRefresh. Отрегулируйте ширину навигатора, чтобы кнопки стали квадратными, как на рисунке выше. А у сетки свойство ReadOnly переведите в True. С главной формой закончили.

    Командой File -> New -> Data Module создайте новый модуль данных. Свойство Name окна переименуйте в fDM, а сам модуль сохраните под именем DM. Перейдите в главное окно, и командой File -> Use Unit подключите к нему модуль DM.

    В модуль поместите следующие компоненты из вкладки InterBase:

    IBDatabase, IBTable, IBQuery, два компонента IBTransaction и один DataSource из вкладки Data Access.

    Займемся настройкой этих компонентов.

    Выделите компонент IBDatabase. Его свойство Name переименуйте в IBDB, чтобы было короче. Откройте свойство Params (откроется окно редактора параметров). Там укажите следующие параметры:

    user_name=sysdba
    password=masterkey
    lc_ctype=win1251

    Обратите внимание, все параметры можно писать маленькими буквами, пробелы перед- и после знака "=" недопустимы. Далее, в свойство DatabaseName найдите и поместите файл нашей базы данных First.gdb. Чтобы при загрузке программы не запрашивался логин и пароль, свойство LoginPrompt переведите в False. После этого свойство Connected переведите в True. Если вы все сделали правильно, база откроется, ошибок не возникнет.

    Теперь займемся компонентами транзакций. Выделите первый IBTransaction, его свойство Name переименуйте в IBTrans1. Этот компонент будет создавать транзакции для считывания данных в сетку и навигатор на главной форме. Откройте свойство Params (откроется окно редактора параметров), и впишите следующие параметры:

    read
    read_committed
    rec_version

    В свойстве DefaultDatabase выберите IBDB, а у IBDB в свойстве DefaultTransaction в свою очередь, выберите IBTrans1. После чего переведите свойство Active транзакции IBTrans1 в True.

    Вторую транзакцию назовите IBTrans2. Она будет нужна только для записи новых данных в таблицу. Поэтому в свойстве Params мы введем следующие параметры:

    write
    concurrency
    nowait

    В свойстве DefaultDatabase этой транзакции также выберите IBDB, а вот свойство Active оставьте False - транзакция будет автоматически запускаться, когда мы попытаемся изменить данные в таблице.

    Перейдем к компоненту IBTable. Свойство Name переименуйте в TTovar, в свойстве Database выберите IBDB, в свойстве Transaction выберите читающую транзакцию IBTrans1. Теперь в свойстве TableName найдите и откройте таблицу TOVAR, после чего свойство Active переведите в True. Таблица открыта.

    Выделите DataSource и переименуйте Name в DSTovar. В свойстве DataSet выберите таблицу TTovar. Теперь можно перейти на главную форму, выделить сетку и навигатор, и в их свойстве DataSource выбрать fDM.DSTovar. Данные должны отобразиться в сетке, а некоторые кнопки навигатора станут недоступны.

    Вернемся в модуль данных. Выделите запрос IBQuery. Свойство Name переименуйте в Q1. В свойстве Database выберите IBDB, а в свойстве Transaction - IBTrans2. Более ничего делать не нужно, запросы будем строить программно.

    Для добавления новых записей и редактирования существующих создадим еще одну форму командой File -> New -> Form. Окно редактора будет очень простым:

    (рис 24.2) Окно редактора

    Форму назовите fEditor, сохраните в папку проекта, дав модулю имя Editor (так как последним мы открывали файл First.gdb, то при попытке сохранения окна Delphi по умолчанию предложит папку с БД. Измените ее на папку с проектом.). Командой File -> Use Unit подключите к редактору модуль DM. На форме расположите два компонента Label, два простых Edit и две кнопки, как на рисунке 24.2. Не помешает в свойстве BorderStyle формы выбрать bsDialog, а в свойстве Position - poMainFormCenter. И не забудьте очистить текст у компонентов Edit, а у кнопок и компонентов Label изменить свойство Caption в соответствии с рисунком.

    Вернемся к главной форме. Командой File -> Use Unit подключите к главной форме модуль Editor. Сгенерируйте событие нажатия на кнопку "Добавить запись". В полученной процедуре впишите код закрытия Q1 (на случай, если ранее запрос был активен), и вызова редактора:

    {Добавить запись}
    procedure TfMain.Button1Click(Sender: TObject);
    begin
      fDM.Q1.Close;
      fEditor.ShowModal;
    end;

    Таким образом, мы будем добавлять новую запись. А для редактирования существующей записи, выделите сетку DBGrid и сгенерируйте для нее событие OnDblClick. То есть, открывать редактор мы будем по двойному щелчку на записи. При этом в таблице TTovar станет текущей нужная нам запись. Прежде, чем вызывать редактор, нам нужно в Q1 получить нужную запись. Для этого мы создадим запрос, откроем Q1 и только потом вызовем редактор. Итак, код события OnDblClick следующий:

    {Двойной щелчок по сетке - редактируем запись}
    procedure TfMain.DBGrid1DblClick(Sender: TObject);
    begin
      fDM.Q1.SQL.Clear;
      fDM.Q1.SQL.Add('select * from Tovar where ID = '+
         IntToStr(fDM.TTovar.FieldByName('ID').AsInteger));
      fDM.Q1.Open;
      fEditor.ShowModal;
    end;

    Как видно из кода, в запросе Q1 мы генерируем запрос вроде такого:

    SELECT * 
    FROM TOVAR
    WHERE ID = 3

    Разумеется, номер ID будет зависеть от того, по какой записи мы щелкнули. При открытии Q1, в нее попадет лишь одна интересующая нас запись. Больше ничего в главной форме делать не нужно. Переходим к окну редактора.

    Тут у нас может быть два варианта: либо мы добавляем новую запись ( Q1 закрыта), либо редактируем существующую ( Q1 открыта и содержит нужную запись). В первом случае нам нужно будет очистить компоненты Edit, если там был текст, а во втором наоборот, вписать в них значения полей Nazvanie и Stoimost.

    В раздел глобальных переменных добавим переменную i, необходимую для хранения ID записи:

    var
      fEditor: TfEditor;
      i: Integer; //идетификатор записи

    Затем выделим форму редактора, и сгенерируем для нее событие OnShow. Код события следующий:

    {При показе редактора}
    procedure TfEditor.FormShow(Sender: TObject);
    begin
      if fDM.Q1.Active then
        i := fDM.Q1.Fields[0].AsInteger
      else i := 0;
      //очищаем или заполняем эдиты:
      if i = 0 then begin
        Edit1.Text := '';
        Edit2.Text := '';
      end //if
      else begin
        Edit1.Text := fDM.Q1.Fields[1].AsString;
        Edit2.Text := fDM.Q1.Fields[2].AsString;
      end; //esle
    end;

    Как видно из приведенного кода, как только форма станет видимой, мы проверяем - активна ли Q1. Если да, то в переменную i прописываем идентификатор текущей записи, иначе i делаем равным нулю. Это нам нужно для того, чтобы в дальнейшем знать, с какой записью работать. Ведь в Q1 будут помещаться другие запросы, и она уже не будет содержать нужную запись.

    Далее, если i = 0 (новая запись), мы очищаем компоненты Edit, иначе считываем в них значения второго и третьего поля запроса ( Nazvanie и Stoimost ).

    Пойдем дальше. Для кнопки "Отменить" введем код простого закрытия формы:

    //закрываем форму:
      Close;

    Так как пользователь может закрыть окно не только кнопкой "Отменить", но и клавишами <Alt + F4> или просто нажав на крестик в правом верхнем углу окна, то проверку на активность транзакции нужно делать в событии формы OnClose.

    Сгенерируем для формы редактора событие OnClose, в котором будем проверять, не в работе ли транзакция IBTrans2, и если да, то сделаем откат транзакции:

    if fDM.IBTrans2.InTransaction then 
        fDM.IBTrans2.RollbackRetaining;

    Прежде, чем перейдем к кнопке "Подтвердить", подумаем вот о чем: для InterBase в вещественных числах между целой частью и дробной должен быть разделитель точка, а пользователь может ввести запятую. Кроме того, в существующих записях разделитель также отображается как запятая, и если мы при показе формы автоматически заполним Edit2, то там тоже будет запятая. Значит, для Edit2 сгенерируем событие OnChange, где устроим простую проверку:

    {Изменение данных в Edit2}
    procedure TfEditor.Edit2Change(Sender: TObject);
    var
      ind: Byte;
      s: String;
    begin
      s:= Edit2.Text;
      for ind:= 1 to Length(s) do
        if s[ind] = ',' then s[ind]:= '.';
      Edit2.Text := s;
    end;

    Здесь, если в тексте будет обнаружена запятая, то она автоматически поменяется на точку. Однако приложение демонстрационное, поэтому мы не делаем проверку на то, что пользователь может ввести не только цифру, точку или запятую, но и какой-нибудь другой символ, например, пробел или букву. Вы можете сделать более детальную проверку, если желаете.

    Нам осталось лишь сгенерировать нажатие на кнопку "Подтвердить". И здесь у нас будет два варианта: либо мы добавляем новую запись ( i = 0 ), и тогда мы используем оператор INSERT, либо мы изменяем существующую. В последнем случае будет применяться оператор UPDATE. Код нажатия на кнопку следующий:

    {Подтвердить}
    procedure TfEditor.Button1Click(Sender: TObject);
    begin
      //очищаем SQL-запрос:
      fDM.Q1.SQL.Clear;
      //создаем новый запрос, в зависимости от показателя i:
      if i = 0 then begin //если i = 0, то это новая запись
        fDM.Q1.SQL.Add('insert into Tovar(Nazvanie, Stoimost)');
        fDM.Q1.SQL.Add('values('+QuotedStr(Edit1.Text)+', '+Edit2.Text+')');
      end //if
      else begin //модифицируем существующую запись
        fDM.Q1.SQL.Add('update Tovar set Nazvanie='+
        QuotedStr(Edit1.Text)+
                        ', Stoimost='+
         Edit2.Text+' where ID ='+IntToStr(i));
      end; //else
      try
        //производим изменения:
        fDM.Q1.ExecSQL;
        //подтверждаем транзакцию:
        fDM.IBTrans2.CommitRetaining;
      except
        ShowMessage('Изменения данных не прошли!');
        fDM.IBTrans2.RollbackRetaining;
      end; //try
      //обновляем НД TTovar:
      fDM.TTovar.Refresh;
      //закрываем форму:
      Close;
    end;
    
    В случае добавления записи формируется запрос типа
    
    INSERT INTO TOVAR(Nazvanie, Stoimost) VALUES('Товар', 10.00)

    В случае редактирования существующей записи формируется другой запрос:

    UPDATE TOVAR 
    SET Nazvanie = 'Товар', Stoimost = 10.00
    WHERE ID = 3

    Разумеется, название товара, его стоимость и идентификатор ID будут зависеть от того, что именно введет пользователь в компоненты Edit, и какую строку в таблице выберет.

    Обратите внимание, что у компонента IBTrans2 вместо методов Commit (подтверждение) и Rollback (откат) используются методы CommitRetaining и RollbackRetaining, которые также подтверждают или откатывают транзакцию, но не закрывают ее при этом, оставляя активной. В многопользовательской среде, где идет активная работа с базой данных, транзакции лучше закрывать, чтобы в базе данных не "висело" множество активных транзакций.

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

    В заключение заметим, что в проектах, которые интенсивно работают с базой данных, совершенно недопустимым будет использование только одной транзакции, это может привести к многочисленным конфликтам. Также недопустимым будет использование отдельной транзакции для каждого набора данных. Находите компромисс: разделяйте наборы данных на читающие, пишущие и НД для отчетов. И для каждой группы используйте отдельный компонент IBTransaction, с соответствующими задаче параметрами.

    Вернуться к учебному плану