Использование генераторов.

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

После прочтения этого руководства, вы узнаете:

  • Как посмотреть, какие генераторы доступны в вашем приложении.
  • Как создать генератор с использованием шаблонов.
  • Как Rails ищет генераторы, чтобы вызвать их.
  • Как Rails генерирует код Rails из шаблонов.
  • Как настроить скаффолд, создавая новые генераторы.
  • Как настроить скаффолд, изменяя шаблоны генератора.
  • Как использовать фолбэки, чтобы избежать переопределения большого набора генераторов.
  • Как создать шаблон приложения.

Первый контакт

При создании приложения с помощью команды rails фактически вы используете генератор Rails. После этого можно получить список всех доступных генераторов, просто вызвав rails generate:

$ rails new myapp $ cd myapp $ bin/rails generate

Вы получите список всех генераторов, поставляющихся с Rails. Если необходимо подробное описание, к примеру, генератора helper, можно просто сделать так:

Наш новый генератор очень прост: он наследуется от Rails::Generators::Base и содержит одно определение метода. Когда генератор вызывается, каждый публичный метод в генераторе выполняется в порядке, в котором он определен. Наконец, мы вызываем метод create_file , который создаст файл в указанном месте с заданным содержимым. Если вы знакомы с Rails Application Templates API, API генераторов покажется вам очень знакомым.

Чтобы вызвать наш новый генератор, нужно всего лишь выполнить:

$ bin/rails generate initializer

Перед тем, как продолжить, давайте посмотрим на описание нашего нового генератора:

$ bin/rails generate initializer --help

Rails обычно способен генерировать хорошие описания, если генератор расположен в пространствах имен, таких как ActiveRecord::Generators::ModelGenerator , но не в этом частном случае. Эту проблему можно решить двумя способами. Первым является вызов desc внутри нашего генератора:

Class InitializerGenerator < Rails::Generators::Base desc "This generator creates an initializer file at config/initializers" def create_initializer_file create_file "config/initializers/initializer.rb", "# Add initialization content here" end end

Теперь можно просмотреть новое описание, вызвав --help на новом генераторе. Вторым способом является добавление описания в файле USAGE в той же директории, что и наш генератор. Мы это сделаем на следующем этапе.

Создание генераторов с помощью генераторов

У самих генераторов есть генератор:

$ bin/rails generate generator initializer create lib/generators/initializer create lib/generators/initializer/initializer_generator.rb create lib/generators/initializer/USAGE create lib/generators/initializer/templates invoke test_unit create test/lib/generators/initializer_generator_test.rb

Вот только что созданный генератор:

Class InitializerGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) end

Сперва обратите внимание, что он унаследован от Rails::Generators::NamedBase вместо Rails::Generators::Base . Это означает, что наш генератор ожидает как минимум один аргумент, который будет именем инициализатора и будет доступным в нашем коде в переменной name .

Это можно увидеть, если вызвать описание для генератора (не забудьте удалить файл старого генератора):

$ bin/rails generate initializer --help Usage: rails generate initializer NAME

Также можно увидеть, что в нашем новом генераторе есть метод класса source_root . Этот метод указывает на место расположения шаблонов нашего генератора, если таковые имеются, и по умолчанию он указывает на созданную директорию lib/generators/initializer/templates .

Чтобы понять, что такое шаблон генератора, давайте создадим файл lib/generators/initializer/templates/initializer.rb со следующим содержимым:

А теперь изменим генератор, чтобы он копировал этот файл при вызове:

Class InitializerGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def copy_initializer_file copy_file "initializer.rb", "config/initializers/#{file_name}.rb" end end

И выполним наш генератор:

$ bin/rails generate initializer core_extensions

Теперь мы видим, что инициализатор с именем core_extensions был создан в config/initializers/core_extensions.rb с содержимым нашего шаблона. Это означает, что copy_file копирует файл из корневой директории исходников в заданный путь назначения. Метод file_name автоматически создается, когда мы наследуем от Rails::Generators::NamedBase .

Доступные для генераторов методы раскрываются в этого руководства.

Поиск генераторов

При запуске rails generate initializer core_extensions Rails затребует эти файлы в следующем порядке, пока один из них не будет найден:

Rails/generators/initializer/initializer_generator.rb generators/initializer/initializer_generator.rb rails/generators/initializer_generator.rb generators/initializer_generator.rb

Если ни один не найден, вы получите сообщение об ошибке.

Вышеуказанный пример положит файлы в папку lib приложения, поскольку сказано, что эта директория принадлежит $LOAD_PATH .

Настройка рабочего процесса

Собственные генераторы Rails достаточно гибки, чтобы позволить вам настроить скаффолд. Они могут быть настроены в config/application.rb , вот несколько настроек по умолчанию:

Config.generators do |g| g.orm:active_record g.template_engine:erb g.test_framework:test_unit, fixture: true end

Так как мы настраиваем наш рабочий процесс, давайте сперва посмотрим, как выглядит наш скаффолд:

$ bin/rails generate scaffold User name:string invoke active_record create db/migrate/20130924151154_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources:users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb invoke test_unit create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder invoke test_unit create test/application_system_test_case.rb create test/system/users_test.rb invoke assets invoke coffee create app/assets/javascripts/users.coffee invoke scss create app/assets/stylesheets/users.scss invoke scss create app/assets/stylesheets/scaffolds.scss

Глядя на этот вывод, легко понять, как работают генераторы в Rails 3.0 и выше. Генератор скаффолда фактически не генерирует ничего, он просто вызывает другие. Это позволяет нам добавить/заменить/убрать любые из этих вызовов. Например, генератор скаффолда вызывает генератор scaffold_controller, который вызывает генераторы erb, test_unit и helper. Поскольку у каждого генератора одна функция, их просто использовать повторно, избегая дублирования кода.

Если хотите избежать генерации файла по умолчанию app/assets/stylesheets/scaffolds.scss при скаффолде нового ресурса, можно отключить scaffold_stylesheet:

Config.generators do |g| g.scaffold_stylesheet false end

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

Если мы сгенерируем другой ресурс с помощью генератора скаффолда, мы увидим, что ни таблица стилей, ни JavaScript, ни фикстуры более не будут созданы. Если мы захотим настраивать его дальше, например использовать DataMapper и RSpec вместо Active Record и TestUnit, это достигается всего лишь добавлением соответствующих гемов в приложение и настройкой ваших генераторов.

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

$ bin/rails generate generator rails/my_helper create lib/generators/rails/my_helper create lib/generators/rails/my_helper/my_helper_generator.rb create lib/generators/rails/my_helper/USAGE create lib/generators/rails/my_helper/templates invoke test_unit create test/lib/generators/rails/my_helper_generator_test.rb

Можно опробовать наш новый генератор, создав хелпер для продуктов:

$ bin/rails generate my_helper products create app/helpers/products_helper.rb

И следующий хелпер будет сгенерирован в app/helpers:

Module ProductsHelper attr_reader:products, :product end

Что, собственно, и ожидалось. Можно сообщить скаффолду использовать наш новый генератор хелпера, снова отредактировав config/application.rb:

Config.generators do |g| g.orm:active_record g.template_engine:erb g.test_framework:test_unit, fixture: false g.stylesheets false g.javascripts false g.helper:my_helper end

и увидев его в действии при вызове генератора:

$ bin/rails generate scaffold Article body:text [...] invoke my_helper create app/helpers/articles_helper.rb

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

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

Для этого мы изменим генератор следующим образом:

# lib/generators/rails/my_helper/my_helper_generator.rb class Rails::MyHelperGenerator < Rails::Generators::NamedBase def create_helper_file create_file "app/helpers/#{file_name}_helper.rb", <<-FILE module #{class_name}Helper attr_reader:#{plural_name}, :#{plural_name.singularize} end FILE end hook_for:test_framework end

Теперь, когда вызывается генератор хелпера, и как тестовый фреймворк настроен TestUnit, он попытается вызвать Rails::TestUnitGenerator и TestUnit::MyHelperGenerator . Поскольку ни один из них не определен, можно сообщить нашему генератору вместо них вызывать TestUnit::Generators::HelperGenerator , который определен, так как это генератор Rails. Для этого нужно всего лишь добавить:

# Search for:helper instead of:my_helper hook_for:test_framework, as: :helper

Теперь можно снова запустить скаффолд для другого ресурса и увидеть, что он также генерирует тесты!

Настройка рабочего процесса, изменяя шаблоны генераторов

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

В Rails 3.0 и выше генераторы не просто ищут шаблоны в корневом пути, они также ищут по другим путям. И одно из них - lib/templates . Поскольку мы хотим изменить Rails::Generators::HelperGenerator , можно это осуществить, просто сделав копию шаблона в lib/templates/rails/helper с именем helper.rb . Так давайте же создадим этот файл со следующим содержимым:

Module <%= class_name %>Helper attr_reader:<%= plural_name %>, :<%= plural_name.singularize %> end

и отменим последнее изменение в config/application.rb:

Config.generators do |g| g.orm:active_record g.template_engine:erb g.test_framework:test_unit, fixture: false g.stylesheets false g.javascripts false end

Если сгенерировать другой ресурс, то увидите абсолютно тот же результат! Это полезно, если хотите изменить шаблоны вашего скаффолда и/или макет, просто создав edit.html.erb , index.html.erb и так далее в lib/templates/erb/scaffold .

Шаблоны скаффолда в Rails часто используют теги ERB; эти теги необходимо экранировать, чтобы сгенерированный результат являлся валидным кодом ERB.

Например, в шаблоне необходим следующий экранированный тег ERB (обратите внимание на дополнительный %)...

<%%= stylesheet_include_tag:application %>

Чтобы сгенерировать следующий результат:

<%= stylesheet_include_tag:application %>

Добавление фолбэков генераторов

Еще одна особенность генераторов, которая очень полезна, это фолбэки. Например, представим, что вы хотите добавить особенность над TestUnit, такую как shoulda . Так как TestUnit уже реализует все генераторы, требуемые Rails, а shoulda всего лишь хочет переопределить часть из них, нет необходимости для shoulda переопределять некоторые генераторы, она может просто сообщить Rails использовать генератор TestUnit , если такой не найден в пространстве имен Shoulda .

Можно с легкостью смоделировать это поведение, снова изменив наш config/application.rb:

Config.generators do |g| g.orm:active_record g.template_engine:erb g.test_framework:shoulda, fixture: false g.stylesheets false g.javascripts false # Добавим фолбэк! g.fallbacks[:shoulda] = :test_unit end

Теперь, если создать скаффолд Comment, вы увидите, что были вызваны генераторы shoulda, но в итоге они всего лишь переуступили генераторам TestUnit:

$ bin/rails generate scaffold Comment body:text invoke active_record create db/migrate/20130924143118_create_comments.rb create app/models/comment.rb invoke shoulda create test/models/comment_test.rb create test/fixtures/comments.yml invoke resource_route route resources:comments invoke scaffold_controller create app/controllers/comments_controller.rb invoke erb create app/views/comments create app/views/comments/index.html.erb create app/views/comments/edit.html.erb create app/views/comments/show.html.erb create app/views/comments/new.html.erb create app/views/comments/_form.html.erb invoke my_helper create app/helpers/comments_helper.rb invoke shoulda create test/helpers/comments_helper_test.rb invoke jbuilder create app/views/comments/index.json.jbuilder create app/views/comments/show.json.jbuilder invoke test_unit create test/application_system_test_case.rb create test/system/comments_test.rb invoke assets invoke coffee create app/assets/javascripts/comments.coffee invoke scss

Фолбэки позволяют вашим генераторам иметь единственную ответственность, увеличить повторное использование кода и уменьшить дублирование.

Шаблоны приложения

Gem "rspec-rails", group: "test" gem "cucumber-rails", group: "test" if yes?("Would you like to install Devise?") gem "devise" generate "devise:install" model_name = ask("What would you like the user model to be called? ") model_name = "user" if model_name.blank? generate "devise", model_name end

В вышеприведенном шаблоне мы определили, что приложение полагается на гемы rspec-rails и cucumber-rails , поэтому они будут добавлены в группу test в Gemfile . Затем мы зададим вопрос пользователю относительно того, хочет ли он установить Devise. Если пользователь ответит "y" или "yes" на этот вопрос, тогда шаблон добавит Devise в Gemfile вне какой-либо группы, а затем запустит генератор devise:install . Затем этот шаблон возьмет пользовательский ввод и запустит генератор devise с переданным ответом пользователя из последнего вопроса.

Представим, что этот шаблон был в файле template.rb . Можно его использовать, чтобы модифицировать результат команды rails new с помощью опции -m и передачей имени файла:

$ rails new thud -m template.rb

Эта команда сгенерирует приложение Thud , а затем применит шаблон к сгенерированному результату.

Шаблоны не обязательно должны храниться в локальной системе, опция -m также поддерживает онлайн шаблоны:

$ rails new thud -m https://gist.github.com/radar/722911/raw/

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

Добавление аргументов командной строки

Генераторы Rails легко модифицировать, чтобы они принимали произвольные аргументы командной строки. Эта функциональность исходит из Thor :

Class_option:scope, type: :string, default: "read_products"

Теперь наш генератор может быть вызван следующим образом.

Большинство SQL-серверов имеет специальные механизмы для создания уникальных идентификаторов - autoincrement, identity, sequence, и т. п. В InterBase и Firebird для этого существует механизм генераторов.

В данной статье будут рассмотрены следующие темы:

Введение

Генераторы предназначены для получения последовательностей уникальных чисел. Например, 1, 2, 3..., 10, 20, 30 и т. п. Такие числа обычно используются как идентификаторы записи в таблицах, имеющих суррогатный (абстрактный)LINK первичный ключ. Например, в таблице клиентов для их нумерации введен столбец CLIENT_ID INTEGER, по которому построен первичный ключ. Столбец можно заполнять значениями генератора.

Нужно сразу заметить, что сами по себе генераторы не обеспечивают сохранение последовательности номеров в случае удаления записей - генератор всего лишь выдает числа по очереди увеличивая их на некоторую величину и обеспечивая уникальность выданных значений. То есть, генератор выглядит как переменная типа integer (в первом диалекте, или int64 в третьем диалекте), которая находится в памяти, и над которой можно выполнять операции Inc и Dec. Если вам требуется обеспечить непрерывные последовательности идентификаторов записей даже в случае их удаления или модификации, то вам нужно обратиться к статье Auditable series of numbers (непрерывные последовательности чисел). В любом случае это задача приложения и сервера, и выходит за рамки данного описания.

Создание генераторов

Генератор - это специальный объект базы данных, который генерирует уникальные последовательные числа. Эти числа могут быть использованы в качестве идентификаторов (например код клиента, номер счета и т. п.). Для создания генератора необходимо использовать оператор DDL

CREATE GENERATOR generatorname;


В Firebird 2.5 альтернативой слову generator является слово sequence.

При выполнении такой команды происходит 2 действия:

  1. На специальной странице БД для хранения значения генератора отводится
    • 4 байта (в первом диалекте или до InterBase 6.0) или
    • 8 байт (в третьем диалекте и во всех современных версиях InterBase/Firebird/Yaffil)
  2. В системной таблице RDB$GENERATORS создается запись, куда помещается имя генератора и его номер (фактически смещение на странице генераторов).
После создания генератора его значения можно получать при помощи функции

GEN_ID(generatorname, inc_value)

Где inc_value - число, на которое необходимо прирастить значение генератора.

Внимание! Получить новое значение генератора можно
1. оператором select, выбрав значение gen_id из таблицы с одной записью, например, системной rdb$database:
select gen_id(my_gen, 1) from rdb$database
2. в триггерах и процедурах - просто присвоив значение gen_id переменной:
myvar=gen_id(my_gen, 1);
3. в запросах - просто вызовом функции gen_id(my_gen, 1)

Генераторы возвращают значения (и сохраняют свои значения на диске) вне контекста транзакции пользователя. Это означает, что если значение генератора было увеличено с 10 до 11 (инкремент 1), то даже при откате транзакции (ROLLBACK) значение генератора, выданное в этой транзакции, не вернется к предыдущему. Вместе с этим гарантируется, что каждому пользователю будет всегда возвращено уникальное значение генератора (вызов функции gen_id всегда происходит "монопольно", то есть непараллельно для любого числа одновременных вызовов, чтобы исключить возможность получения разными пользователями одного и того же значения).

Примечание. Приращение значения генератора производится монопольно, аналогично функции InterlockedIncrement в Win32 API. То есть, даже если 1000 пользователей одновременно вызовут gen_id(x, 1), то они получат каждый свое (уникальное) значение (от x+1 до x+1+1000).

При выборке значения генератора запросом вида select gen_id(genname, x) from ... следует учитывать буферизацию выборки на клиенте. Т.е. в многопользовательской среде при выполнении двух таких запросов значения генератора будут увеличиваться "пачками", а не на величину x для каждой выбираемой записи.

Примечание. Иногда находятся умельцы, которые пишут
select gen_id(a, 0) + 1 from rdb$database
или
tmp=gen_id(a, 0);
tmp=tmp+1;
и др. аналогичные варианты. Если непонятно, почему так делать нельзя, прочитайте примечание выше еще раз.

Использование генераторов

Работа с генератором возможна только при помощи встроенной функции gen_id, а значит работать с генераторами можно в запросах, процедурах и триггерах. Давайте создадим генератор, и посмотрим, как его можно использовать.

CREATE GENERATOR NEWID;


В этот момент в базе создан новый генератор. Его значение равно 0. Если вам нужно использовать другое начальное значение, то можно установить (изменить) значение генератора следующей командой

SET GENERATOR NEWID 1000;


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

Обычно "начальное" значение генератора устанавливают в отличное от нуля, когда база данных является "распределенной", то есть в разных филиалах могут создавать новые записи в какой-либо таблице. Например, в главном офисе нумерация начинается с 0, в первом филиале - с 100000, во втором - с 200000, и так далее. Такой подход позволит избежать ситуаций, когда в двух филиалах созданы разные записи с одинаковым идентификатором.

Другой случай - наличие готового справочника с фиксированными идентификаторами. В этом случае вам нужно установить значение генератора больше максимального существующего идентификатора такого справочника.

В общем, установка начального значения генератора <> 0 зависит только от специфических требований приложения.

Увеличение значения генератора

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

GEN_ID(NEWID, 1);


В определенных случаях может потребоваться больший шаг значений, например 10, если в системе возможны ситуации, когда нужно "вставить" идентификатор между двумя соседними. В этом случае шаг генератора выбирается > 1, а для вставки идентификатора "посередине" ему присваивается промежуточное число, без использования генератора. Например, в таблице автоматически назначены идентификаторы 10, 20 и 30. Для "вставки" нового идентификатора между 10 и 20 ему присваивается номер 15 (уже без использования генераторов, естественно).

Увеличение значения генератора функцией GEN_ID производится в монопольном режиме. Это означает, что одновременный вызов GEN_ID(NEWID, 1) двумя приложениями вернет каждому только свой идентификатор. Пример:

генератор имеет значение 7
приложение1: gen_id(newid, 1) -- выдан номер 8
приложение2: gen_id(newid, 1) -- выдан номер 9
...

Select

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

SELECT GEN_ID(NEWID, 1) FROM RDB$DATABASE


rdb$database здесь используется как таблица, содержащая только одну запись. В результате этим запросом также будет выдана 1 запись со значением генератора, увеличенным на 1.

Такой же способ используется в компонентах IBX LINK и FIBPlus - у IBDataSet есть метод GeneratorField, который позволяет обеспечить автоматическое присвоение нового номера столбцу первичного ключа записи как раз при помощи указанного оператора select, выполняемого библиотекой компонент "прозрачно". В FIBPlus та же самая функциональность устанавливается при помощи FIBDataSet.AutoUpdateOptions (GeneratorName).

Если вы по каким то причинам не хотите или не можете пользоваться методом GeneratorName, то можно взять отдельный компонент IBSQL (или IBQuery), прописать в нем запрос получения нового значения генератора, и вызывать такой запрос перед вставкой новой записи в таблицу.

Триггер

Однако, начинающие разработчики чаще пытаются использовать генератор в триггере, примерно следующим способом:

CREATE TRIGGER TBI_CLIENTS FOR CLIENTS
ACTIVE BEFORE INSERT POSITION 0
AS

BEGIN

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

Конечно, триггер можно оставить, изменив лишь код

IF (NEW.CLIENT_ID IS NULL) THEN

NEW.CLIENT_ID = GEN_ID(NEWID, 1);

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

Примечание. Первая попытка перенести ответственность за автоматическую нумерацию столбца первичного ключа таблицы обычно проваливается из-за компонент доступа. Поскольку такой столбец объявлен как not null, и компоненты автоматически считывают характеристики столбцов, у TField будет установлено в True свойство Required. Это не дает возможности оставить столбец "пустым" при передаче с клиента на сервер. Установите свойство Required у такого столбца в False.

Примечание. Данная проблема может быть решена в Firebird 2.0, при помощи оператора INSERT...RETURNING. Однако если вручную (через IBSQL, IBQuery) такой оператор можно выполнить и получить из него результат, то для IBDataSet это может оказаться невозможным, поскольку IBDataSet или IBQuery+IBUpdateSQL просто не будут знать, что оператор вставки что-то возвращает. Если от FIBPlus можно ожидать поддержки insert...returning после выхода Firebird 2.0, то от IBX - вряд ли, если только аналогичная функциональность будет реализована в InterBase.

Процедура

Использование генераторов в процедурах ничем не отличается от использования генераторов в триггерах. Вот пример процедуры, которая возвращает новое значение генератора:

CREATE PROCEDURE GETNEWCLIENT
RETURNS (NID INTEGER)
AS

BEGIN

NID = GEN_ID(NEWCLIENT, 1);
SUSPEND;


Чтобы получить результат такой процедуры, можно ее выполнить как

SELECT NID FROM GETNEWCLIENT


Также, если не указывать в процедуре SUSPEND, можно получить значение из процедуры путем

EXECUTE PROCEDURE GETNEWCLIENT RETURNING_VALUES:param


Этот способ отличается от обращения к rdb$database тем, что вам приходится создавать новую процедуру на каждый генератор. Если вдруг потребовалось создать одну процедуру для получения новых значений из разных генераторов, то можно воспользоваться двумя способами. Первый способ - просто обработка if

CREATE PROCEDURE GETNEWID
(GENID INTEGER)
RETURNS (NID INTEGER)
AS
BEGIN

IF (:GENID = 1) THEN

NID = GEN_ID(NEWCLIENT, 1);

IF (:GENID = 2) THEN

NID = GEN_ID(NEWORDER, 1);

IF (:GENID = 3) THEN

NID = GEN_ID(NEWSALE, 1);


Можно такую процедуру более "универсализировать" при помощи оператора EXECUTE STATEMENT в Firebird 1.5, который позволяет выполнять запросы динамически в процедурах и триггерах.

CREATE PROCEDURE GETNEWID
(GEN VARCHAR(30))
RETURNS (NID BIGINT)
AS
DECLARE VARIABLE SQL VARCHAR(60);
BEGIN

SQL = "SELECT GEN_ID("||GEN||", 1) FROM RDB$DATABASE;
EXECUTE STATEMENT SQL INTO:NID;
SUSPEND

END

Обратите внимание, что возвращаемая переменная NID имеет тип BIGINT, в отличие от предыдущих примеров - execute statement требует точного соответствия типов переменных. Если требуется получить результат в тип integer, то следует применить операцию cast(var as integer) либо сразу в запросе, либо к выходной переменной после того, как значение получено из execute statement.

Следует помнить, что execute statement производит динамическое выполнение запроса, а значит это будет чуть медленнее, чем выполнение такого запроса с клиента. Пример вызова процедуры (suspend в процедуре опять же указан для возможности ее вызова как select):

SELECT * FROM GETNEWID("NEWCLIENT");
SELECT * FROM GETNEWID("NEWORDER");
SELECT * FROM GETNEWID("NEWSALE");


Как видите, этот изощренный способ опять же ничем не лучше, чем просто вызовы

SELECT GEN_ID(NEWCLIENT, 1) FROM RDB$DATABASE;
SELECT GEN_ID(NEWORDER, 1) FROM RDB$DATABASE;
...

Экзотические "глюки"

  1. Экзотическим применением генератора является например его указание в default для столбца таблицы. В результате при restore сервер будет вызывать default столько раз, сколько есть записей в таблице, и значение генератора после restore окажется равным старому (до backup) плюс число записей в таблице. Данное поведение справедливо для всех версий InterBase/Firebird/Yaffil, и будет исправлено в Firebird 2.0.
  2. "Двоение" генераторов, то есть увеличение не на +1, а на +2, можно элементарно получить, если
    • вы сначала использовали генератор в триггере (без условия if)
    • прочитав эту статью, решили использовать select gen_id(x, 1) from rdb$database

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

  1. Вызов gen_id(x, y) в процедуре или триггере всегда возвращает 0.

Это может быть связано с тем, что генератор удален и создан снова. При этом в коде blr процедуры или триггера, которая явно ссылается на имя генератора, осталась ссылка на старый идентификатор генератора, в то время как в rdb$generators этот генератор уже имеет новый идентификатор.
Для решения этой проблемы нужно сделать alter trigger/procedure, чтобы перекомпилировался blr.

Примечание. Данная проблема вовсю существовала во времена повсеместного использования SQLExplorer (инструмента BDE). Он при попытке изменить значение генератора почему то удалял и создавал генератор снова.


Сброс значения генератора

Переустановка значения генератора на какое то новое значение, которое отличается от его обычного инкремента, может производиться теми же способами, которые были указаны выше - как при помощи оператора SET GENERATOR так и при помощи функции GEN_ID.

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

SET GENERATOR NEWCLIENT TO 0;


Это обычный ddl оператор. Он не может быть выполнен в процедуре или триггере (за исключением определенных версий Yaffil). Если все же сброс генератора требуется совершить в некоей процедуре (как уже говорилось, в триггерах такие действия выполнять категорически не рекомендуется), то это можно выполнить при помощи gen_id:

...
TEMPVAR = GEN_ID(NEWCLIENT, -GEN_ID(NEWCLIENT, 0));
...

То есть, генератор увеличивается на его же текущее отрицательное значение.

Получение "текущего" значения генератора

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

SELECT GEN_ID(NEWCLIENT, 0) FROM RDB$DATABASE


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

Удаление генераторов

Оператор DROP GENERATOR X появился только в Firebird 1, Yaffil и InterBase 7.1. В предыдущих версиях в языке DDL оператора для удаления генератора не было. Тем не менее, серьезной проблемы это не представляло. В самом начале статьи было упомянуто, что запись о генераторе создается в таблице RDB$GENERATORS. Эту запись, безусловно, можно удалить. Однако место, распределенное на странице генераторов, освобождено не будет. Оно будет освобождено (точно так же и для drop generator) только после того, как вы сделаете вашей БД BACKUP/RESTORE.

Backup/Restore

Значения генераторов сохраняются при backup, и восстанавливаются при restore. Однако, если делать бэкап только метаданных (или restore только метаданных), то в некоторых версиях Interbase/Firebird/Yaffil значения генераторов могут оказаться "мусорными". Эта ошибка исправлена в самых последних версиях Firebird и Yaffil.

Если backup/restore метаданных используется для создания "клона" базы данных, используемого в другой фирме, офисе и т. п., то вам придется самостоятельно или обнулить генераторы, или установить их в требуемые значения.

Сбои в работе

Как уже говорилось выше, генераторы не подвержены влиянию транзакций, и казалось бы, должны инкрементироваться всегда. Увы, иногда при сбоях сервера (выключение питания, падение ОС и т. п.) страницы генераторов не успевают сохраниться на диск, хотя данные, записанные по commit, сохраняются. Более вероятна такая ситуация при Forced Write = Off.

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

Это исправлено в Firebird 1.5.2, то есть страницы генераторов записываются на диск до записи страниц с данными. На других версиях InterBase/Firebird/Yaffil с данной проблемой можно бороться только написав специальную программу "восстановления" или контроля значений генераторов после сбоя, которая будет содержать проверку на select max(id) from table и установку соответствующего генератора в это значение.

Типичные ошибки в использовании генераторов

При использовании генераторов в процедурах и триггерах разработчик иногда забывает, что в них может быть использован процедурный язык, и постоянно получает новое значение генератора путем обращения к rdb$database

declare variable i int;
begin

SELECT GEN_ID(X, 1) FROM RDB$DATABASE
INTO:I;

...


В данном случае в select нет никакой необходимости. Текст можно сильно упростить

declare variable i int;
begin

I = GEN_ID(X, 1);
INSERT INTO TBL VALUES (:I...
...


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

begin

INSERT INTO TBL VALUES (GEN_ID(X, 1), ...
...


Собственно, ошибкой первый пример кода не является.

Встречаются случаи избыточного преклонения перед примером с rdb$database:


values (select gen_id(mygen, 1)from rdb$database, :param...)


Помилуйте, gen_id - это функция, поэтому здесь достаточно такого:

insert into table (field1, field2...)
values (gen_id(mygen, 1), :param...)


Собственно, такой вызов gen_id не приветствуется - нет гарантий, что вставка в эту таблицу будет производится только этим оператором, прописанным в одном единственном месте кода вашего приложения (опять же единственного, работающего с этой БД).

Более существенная и грубая ошибка в использовании генераторов - это увеличение "текущего" значения генератора вне контекста gen_id. Схематично выглядит как

I = GEN_ID(X, 0);
I = I + 1;

Или еще более безумный вариант

SELECT GEN_ID(X, 0) + 1 FROM RDB$DATABASE


Понятно, что время между получением текущего значения генератора и его увеличением очень мало, но все равно нельзя исключать, что в многопользовательской среде два таких блока кода, выполняемые параллельно, могут получить одно и то же значение генератора (в во втором случае вероятность куда больше, чем в первом, потому что неизвестно, когда ваш код поместит полученное таким образом значение на сервер). В начале статьи было указано, что функция GEN_ID гарантирует получение уникального значения при инкременте <> 0 путем "монополизации" вызова функции с использованием объектов операционной системы типа mutex. В приведенных вариантах защиты от параллельного вызова нет.

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

Нестандартное применение генераторов

Вы уже видели, что функцию GEN_ID можно использовать в операторе SELECT. Вот как можно получить количество записей, выбранных запросом:

SET GENERATOR MYGEN TO 0;
SELECT GEN_ID(MYGEN, 1), FIELD1, FIELD2, FIELD3, ... FROM MYTABLE.


Такой запрос вернет в качестве первого поля порядковый номер записи, и после выполнения запроса генератор MYGEN будет содержать количество возвращенных записей. Кроме этого, во время выполнения этого запроса любой другой пользователь этой же БД может получить текущее значение генератора MYGEN и узнать сколько записей уже выбрано запросом на текущий момент (нечто вроде ProgressBar, однако число записей все-равно неизвестно до окончания выполнения запроса).

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

Функцию GEN_ID можно также использовать и как "выключатель" длительных запросов. Пример приведен для БД EMPLOYEE.GDB.
Фактически такой запрос означает - "выбирать записи пока значение генератора = 0". Как только другой пользователь или ваше приложение в другом коннекте выполнит операцию

SET GENERATOR EMP_NO_GEN TO 1;

Запрос прекратится, т. к. условие WHERE станет равным FALSE.

Обязательно учтитывайте буферизацию записей клиентской частью (gds32.dll) или сервером при выполнении подобных запросов. Например, приведенный выше запрос с проверкой генератора в where "выключится" не сразу, а через некоторое время. Перед применением такого метода в вашей системе сначала следует проверить, подходит ли он вообще для выполняемого вами запроса.

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

  • Клиентское приложение при запуске определяет, есть ли для него (например в Registry или INI-файле) "именной" генератор.
  • Если нет, то оно операцией SELECT GEN_ID(GlobalGen, 1) FROM RDB$DATABASE получает идентификатор (например 150), создает на сервере собственный генератор операцией CREATE GENERATOR USER_N; (например, USER150). После чего сохраняет имя этого генератора на локальном диске.
  • Если да, то приложение обнуляет "именной" генератор операцией SET GENERATOR ... TO 0; (в нашем примере SET GENERATOR USER150 TO 0;) и выдает запросы с использованием данного генератора.
Этот способ не может быть рекомендован для систем с большим числом пользователей или частыми коннектами-дисконнектами. В ранних версиях InterBase был баг, который приводил к ошибке при определенном числе генераторов в базе данных (128 при размере страницы 1К). Даже и без бага этот способ приведет к постоянному росту числа страниц, выделенных под генераторы.

При помощи генераторов можно также решить проблему с отсутствием временных таблиц в вашей версии сервера (временные таблицы появились в InterBase 7.5LINK). Вы создаете таблицу, например TEMP_TBL, и в качестве первого поля, входящего в первичный ключ, указываете поле типа INTEGER. Пользователь при соединении с сервером получает собственный идентификатор у некоторого общего генератора, и затем использует его при помещении записей в такую "временную" таблицу. В результате, даже если несколько пользователей будут работать с такой таблицей, они никогда не "пересекутся" по значению первого поля первичного ключа "временной" таблицы..

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

create procedure GENREPORT
as
declare variable i int;
begin

i = GEN_ID(REPGEN, 1); -- проверим, запущена ли процедура
if (i > 1) then -- да, процедура уже работает

begin

i = GEN_ID(REPGEN, -1); -- возвращаем значение обратно
EXCEPTION REPORT_ALREADY_RUNNING;

Обработка отчета
i = GEN_ID(REPGEN, -1); -- отчет закончен, возвращаем значение обратно


Смысл очень прост. Исходное значение генератора = 0. При запуске процедуры она его увеличивает на 1. Если при проверке значения генератора окажется, что он больше 1, то это означает что копия процедуры уже запущена и работает.

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

  1. обработка ошибок по when any в процедуре, с "возвратом" значения генератора
  2. обработка ошибки из процедуры на клиенте, и явная выдача запроса

select gen_id(repgen, -1) from rdb$database с клиента в случае неуспешного выполнения процедуры.


Разумеется, на всякий случай следует еще проверять, не остался ли генератор в значении < 1 после его увеличения на 1 в начале процедуры. И обязательно предусмотреть обработку ситуации, когда используется 2-ой случай решения проблемы с ошибкой в процедуре, и в результате ошибки потеряно соединение с сервером.

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

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

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

Производство дешевой электроэнергии

Электрическая энергия нужна любому современному человеку. Ведь без многих бытовых устройств уже невозможно существовать, а все они практически работают от электричества. Желание сэкономить некоторых уже привело к приобретению ветряных генераторов, которые способны создавать ценный ресурс уже при воздушном движении в 8–9 м/сек. Причем такого генератора энергии из воздуха хватает на длительное время. Стоимость устройства отличается в зависимости от:

  • Накапливаемого объема электроэнергии
  • Материалов, из которых оно изготовлено
  • Способности реагировать на движения ветра
  • Есть умельцы, которые создают аналогичные генераторы из подручных материалов

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

Технологические особенности ветряного генератора

Ветряной генератор может в качестве основы иметь лопасти или барабанные цилиндры. Работает по принципу вертикальной или горизонтальной оси. Чаще всего используют ветряной генератор с горизонтальной осью. Основными элементами устройства являются:

  • Коробка передач
  • Вал ротора
  • Система торможения
  • Генератор, преображающий энергию в электрическую.

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

Работает такое устройство достаточно просто. Когда ветер поворачивает лопасти или половинки барабана, вращается вал. Он соединяется с генератором, который по законам физики превращает кинетическую энергию в электрическую. Та, в свою очередь, передается или непосредственно на объект или накапливается специальным оборудованием, чтобы отдать энергию в нужный момент.

Как сделать ветряной генератор самостоятельно

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

В случае создания лопастей из кровельного железа, его нужно хорошо обработать. Края же лопастей требуют дополнительного утяжеления. Обычно прикрепляют к лопастям металлические прутья. Диаметр прутьев подобрать в 5–6 мм.

Лопасти из фанеры покрываются олифой. Это защитит ее от разрушений. Толщина фанеры должна быть не менее 5 мм.

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

Элементы барабана следует тщательно промазывать места стыков. Делать это лучше масляной краской.

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

Соединяющие лопасти крестовины лучше заказать сварщикам. Их делают из стальных полосок размером 5 на 60 мм. Можно использовать древесину, но такой вариант менее долговечен и надежен.

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

Ювелирная работа требуется при проведении подгонки трубы к подшипникам.

Нужно замерять расстояние между лопастями и осью. Оно должно быть:

  • Одинаковым
  • Не менее 140 мм

После этого уже создается основание, на котором устанавливаются шариковые подшипники. В качестве подставки может подойти:

  • Металлический угол
  • Металлическая пластина
  • Старая шестеренка

Основание должно быть подвергнуто сверлению. Число отверстий рассчитывается как два на лопасть. Лопасти крепятся с помощью специальных болтов.

Созданный таким образом генератор позволяет создать передаваемую мощность примерно 800 Вт, если скорость ветра достигает в среднем 9–11 м/с.

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

Производство или перепродажа

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

Можно также привлечь другого мастера для создания генераторов. В случае выбора посреднической схемы есть свои недостатки:

  1. Высокая степень риска, что конкуренты выйдут на такого же поставщика или специалиста.
  2. Вознаграждение за посредничество обычно меньше, чем надбавка за созданный продукт.
  3. Эмоциональные затраты на работу с посредниками выше. Могут срываться сроки, качество не соответствовать ожиданиям и так далее.

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

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

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

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

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


Статьи по теме: