Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

JAVA. Серверные приложения

.pdf
Скачиваний:
0
Добавлен:
06.09.2026
Размер:
2 Мб
Скачать
Часть 5. Работа с базами данных при помощи Java 281
бизнес-правила, применяемые к бизнес-объектам, и не всегда эти биз­нес-объекты имеют базовый тип, используемый в СУБД. В таблице
39.2.1 перечислены основные SQL-типы, их представления в СУБД DB2 и Java примитивы отражающие основные типы данных.
Таблица 39.2.1
Òèï Java òèï SQL DB2
boolean BIT SMALLINT
byte TINYINT SMALLINT
char CHAR(1) CHAR(1)
double DOUBLE FLOAT
float REAL REAL
int INTEGER INTEGER
long BIGINT BIGINT
short SMALLINT SMALLINT
String VARCHAR VARCHAR(2000)
java.math.BigDecimal NUMERIC DECIMAL
byte[] VARBINARY BLOB
java.sql.Date DATE DATE
java.sql.Time TIME TIME
java.sql.Timestamp TIMESTAMP TIMESTAMP
java.util.Date TIMESTAMP TIMESTAMP
java.io.Serializable VARBINARY BLOB
И все-таки иногда, а в последнее время все чаще, всех поддерживае-
мых типов не хватает для работы приложений, и для этой ситуации предусмотрена возможность создания пользовательских типов данных user-defined types (UDTs).
Комбинация совместного использования UDT и LOB дает в руки раз-
работчика существенные преимущества при работе с данными. LOB по­зволяет избежать трудоемкой работы по разделению данных на состав­ные части и раскладыванию этих частей по разным столбцам. Исполь­зование UDT позволяет создавать комбинированные типы данных и решает проблему отсутствия необходимых Web-приложению типов. В добавление к вышесказанному можно добавить еще несколько преи­муществ использования UDT.
Поддержка объектно-ориентированного программирования DB2 по-
зволяет группировать родственные объекты в связанные типы данных. Эти типы имеют название (имя), внутреннее представление и опреде­ленное поведение. Используя UDT, можно сообщить DB2 название (имя) нового типа и как это представлено внутренне. LOB — одно из возможных внутренних представлений для нового типа и наиболее под­ходящее представление для больших, сложных структур данных.
282 Часть 5. Работа с базами данных при помощи Java
В объектно-реляционной системе важно не только отслеживать со-
стояние сохраняемых объектов, но и их поведение. Таким образом, по­ведение данных представляет собой ресурс, который может совместно использоваться несколькими приложениями.
Строгая типизация объектов, используемая в DB2, гарантирует, что
определенные типы могут работать только со своим функциями. В DB2 поведение для определенных типов можно описать при помощи пользо­вательских функций.
Такая банальная вещь, как складывание двух чисел, может вызвать
большой скандал, если эти числа представляют различные мировые ва­люты и производится обычная выборка по стоимости. Разницу между японской йеной и американским долларом объяснять не нужно, но ти­пов данных, отражающих денежный эквивалент в современной версии DB2, не существует, да они и не нужны. В других СУБД величины представлены особым типом MONEY. Для решения этой задачи можно воспользоваться возможностью, предоставляемой СУБД, по использова­нию пользовательских типов.
Предоставляя пользователям возможности определять собственные
типы данных, а также возможности определять правила, управляющие поведением активных данных, имеется возможность повысить ценность хранимых данных за счет расширения их семантического содержания. Тенденция расширения семантического сокращения хранимых данных, проще говоря, использование UDT, является на сегодняшний день наи­более важным направлением развития в области управления базами данных.
UDT — это механизм для моделирования и хранения сложных типов
данных в СУБД. У UDT может быть несколько полей, например, для геометрического объекта — это список координат точек. Поэтому в раз­личных системах управления базами данных DB2 не исключение, встроена поддержка создания пользовательских типов данных, именуе­мых как особый тип (distinict type). Каждый из особых типов порожден от одного из базовых типов, но имеет собственный набор допустимых операций. После создания особого типа единственными операторами применяемые к данным этих типов являются операции сравнения с данными того же типа. Очень просто указать операции, используемые при работе с особыми типами. Стандарт SQL позволяет переопределять функции для разрешения применения оператора к особому типу. Необ­ходимо просто создать новую функцию, назвать ее так же, как сам опе­ратор, описать тело и указать в виде входящих и выходящих парамет­ров значения особого типа.
В DB2 расширена поддержка UDT — это позволяет создавать табли-
цы только из UDT. Кроме того, теперь в одном объекте UDT можно ис­пользовать и другие типы UDT. Это значит, что атрибуты UDT больше не ограничиваются базовыми типами SQL. Например, в DB2 описаны функции преобразования, трансформирующие сложную структуру опи­санных в пользовательском приложении в стандартные базовые типы SQL. Эти функции обеспечивают и обратное преобразование базовых типов SQL в UDT. Эти преобразования нужны для перемещения UDT в базу данных и обратно.
Часть 5. Работа с базами данных при помощи Java 283
UDT подобен другим объектам базы данных, такие, как таблицы, ин-
дексы, создаются оператором CREATE. Используя CREATE DISTINCT TYPE, оператор определяет новый тип UDT.
UDT основаны на встроенных типах. При создании UDT необходимо
определить родовой тип, от которого порожден пользовательский и опе­раторы, работающие с данным типом. DB2 не позволяет использовать созданные пользовательские типы для порождения других типов.
Для создания пользовательского типа используется оператор DB2
CREATE DISTINCT TYPE.
Пользовательский тип деньги определен от предназначенного для
различных обработок денежных единиц. Основное здесь — наличие двух цифр после запятой. Для разных значений необходима возмож­ность сравнения. Ниже созданы пользовательские типы отражения раз­личных денежных единиц.
CREATE DISTINCT TYPE us_dollar AS DECIMAL (9, 2) WITH COMPARISONS CREATE DISTINCT TYPE euro AS DECIMAL (9, 2) WITH COMPARISONS
CREATE DICTINICT TYPE polygon AS BLOB(1M)
Фраза WITH COMPARISONS указывает, что два значения в долла-
рах могут сравниваться между собой, так же как два значения в йенах, но значения доллара не могут сравниваться со значениями йены, хотя оба типа имеют базовым один тип — DECIMAL. Кроме того, ни одно из этих значений нельзя сравнивать с любым объектом, также представ­ляющим базовый тип DECIMAL.
Еще один пример иллюстрирует пользовательский тип, описываю-
щий работу и имеющий тип CLOB с размером в 32 кбайт.
CREATE DISTINCT TYPE personal.application_form AS CLOB(32K) При создании UDT СУБД DB2 генерирует функцию преобразования
созданного типа к базовому и обратно. После создания UDT единствен­ными, которыми можно пользоваться при работе с созданным UDT, яв­ляются стандартные операторы сравнения с данным того же типа. Для расширения возможностей использования типов необходимо явно ука­зать операции управления созданными пользовательскими типами. Можно просто указать, какие из операторов базового типа имеют смысл при применении к определенным созданным типам. Каждый из опера­торов, таких, как сложение (+), реализован в виде функции, имеющей то же имя, что и оператор. Чтобы разрешить использование данного оператора к созданному типу данных, необходимо просто создать новую функцию, которая имеет то же имя, что и оператор, но возвращаемым значением является UDT как входящий, также являющийся созданным UDT.
CREATE FUNCTION «+» (DOLLARS, DOLLARS) RETURNS DOLLARS SOURCE SYSIBM. «+» (DECIMAL(), DECIMAL()); CREATE FUNCTION «*» (INTEGER, DOLLARS) RETURNS DOLLARS SOURCE SYSIBM. «*» (INTEGER(), DECIMAL());
284 Часть 5. Работа с базами данных при помощи Java
В качестве реализации функции-оператора можно указать как ис-
точник реализации SOURCE SYSIBM. «+» (DECIMAL(), DECIMAL()); предоставляемая СУБД для базового типа, т. е. операция с UDT, будет производиться так же, как она производится для базового типа. В вы­шеуказанном примере создаются две функции. Первая предназначена для сложения двух значений DOLLARS, а во второй функции опреде­ляется операция умножения для значений DOLLARS. Здесь необходимо учесть один маленький нюанс. Умножение двух значений DOLLARS может дать неправильный результат, поскольку оба представляют де­сятичное значение, в то время как использование пары INTEGER, DECIMAL сохранит значение после десятичной точки при умножении. Но описанная функция создалась именно для умножения различных чисел со значением DOLLARS, и если пользователь попробует умно­жить два значения DOLLARS, то СУБД выдаст сообщение о возникшей ошибке, так как функция по умножению значений DOLLARS не опре­делена.
За счет использования реализации встроенных функций можно чет-
ко указать набор операторов, имеющих смысл для UDT. Конечно, ино­гда может потребоваться добавить особенные свойства к создаваемым UDT, помимо операторов базового типа. Можно написать функцию на Java и зарегистрировать в базе данных. DB2 содержит около 100 функ­ций для выполнения самых различных операций с числами, строками, датами и другими базовым типами. Но для описания поведения UDT используются функции, определенные пользователем. В нашем кон­кретном случае для описания поведения UDT будет использоваться язык программирования Java.
39.3. SQLJ и функции определенные пользователем (UDF)
В современных СУБД определены соглашения по передаче парамет-
ров определяемой пользователем функции (user defined function UDF) в предоставляемую пользователем программу, реализующую данную функцию. Пользовательские функции являются прекрасным подспорь­ем при создании приложений по обработке данных, которым не хватает стандартных средств. UDF создаются на любом языке программирова­ния — Visual Basic, C/C++ или Java. Приложения, реализованные на языке программирования, содержат синтаксические конструкции базо­вого языка, в которые включены любые SQL-операторы, возвращающие простые скалярные значения или таблицы. Использование различных языков программирования при создании UDF еще точно не стандарти­зировано, но для самых популярных языков, таких, как JavaиС++, уже описаны определенные правила, конкретные для каждой версии различных СУБД. Использование конкретного языка зависит от воз­можностей СУБД по поддержке данного языка.
Создание пользовательских функций User-Defined Functions (UDFs)
Создание пользовательской функции на Java состоит из нескольких
шагов, которые практически не отличаются от этапов создания обычно-
Часть 5. Работа с базами данных при помощи Java 285
го приложения. Любые Java клиентские приложения, написанные с ис­пользованием SQLJ или JDBC, могут вызывать UDF.
Написать UDF на используемом языке, например Java, содержа-
щем SQL-операторы Компилировать UDF на соответствующем компиляторе. В случае
использования Java, это должен быть стандартный java-компиля­тор, входящий в пакет JDK. В пакете разработчика DB2 имеется собственный java-комилятор.
После создания функцию необходимо зарегистрировать в базе дан-
ных. В DB2 существует специальный SQL-оператор, выполняющий роль регистратора, — CREATE FUNCTION. Если предполагается использование функции более чем в одной базе данных, оператор CREATE FUNCTION выполняется для каждой зарегистрированной базы данных. Оператор CREATE FUNCTION определяет типы па­раметров и тип возвращаемого значения. Одной из особенностей UDF, используемых в DB2, является управление расположением пользовательской функцией в системной области памяти DB2. Для этого в DB2 имеются SQL-операторы FENCED или NOT FENCED. Для более подробного ознакомления с возможностями данных опе­раторов следует обратиться к руководству использования SQL в документации на DB2. Очень важно защитить исполняемый файл, реализующий функции, так как его исполнение производится при вызове функции без последующей проверки системой.
Перед началом использования созданной функции желательно протестировать работу, производимую UDF.
Особое внимание уделяется созданию пользовательских функцией,
поскольку большую часть работы можно выполнить в самой базе дан­ных. Использование сторонних приложений в бизнес-логике приводит к значительному увеличению обработки SQL-запросов. Скорость работы Web-приложения значительно увеличивается при правильном проекти­ровании компонентов. Компонент, ответственный за бизнес-логику, дол­жен выполняться в базе данных, а не во внешних компонентах, какими бы быстрыми компоненты ни были.
Комбинацию имени функции, тип ее параметров называют прототи-
пом функции. Что под этим подразумевается? Большинство современ­ных языков программирования — и в данном случае SQL не исключе­ние — имеют встроенную возможность перегружать функции, т. е., ис­пользуя одно имя функции, передавать ей различные аргументы. При вызове конкретной функции СУБД активизирует ту функцию, чьи ар­гументы наиболее точно соответствуют действительным типам аргумен­тов.
Хранимые процедуры и функции определяют действия пользователя
при работе с базой данных. Вызов пользовательской функции осущест­вляется посредством стандартного SQL-оператора CALL. SQLJ не на­кладывает никаких ограничений по вызову функций созданных на дру­гом языке. Так что оператор CALL можно использовать в SQLJ-прило­жении, без каких-либо доработок и вызывать любую функцию или процедуру, реализованную даже не на Java.
286 Часть 5. Работа с базами данных при помощи Java
#sql { CALL MY_PROCEDURE(:arg) }; {intx; #sqlx={VALUES( F(name) ) }; }
Хранимые процедуры имеют три типа параметров: IN, OUT, INOUT.
Параметры типа IN передаются в хранимую процедуру и являются JA­VA-переменными. Возвращаемые значения являются типами OUT и INOUT и представляют собой массивы данных. О создании хранимых процедур для каждой конкретной СУБД лучше посмотреть документа­цию, поставляемую производителем базы данных. В DB2 используется оператор CREATE PROCEDURE, в котором содержатся имя процедуры, аргументы, местоположение.
В DB2 реализована поддержка создания пользовательских функций
на языке Java. Пользовательские функции, используемые в DB2, со­зданные на языке Java, работают с любыми данными, определенные по­льзователем, могут использоваться в любом SQL-выражении.
Создание пользовательских функций с использованием Java допол-
няет SQLJ-приложение различными особенностями. Кроме LOB и UDT, гибкость приложению можно задать, используя различные типы пове­дений для каждого объекта базы данных. Создание пользовательской функции при помощи Java определяется следующим шагами:
Функция создается как Java-метод. Затем производится стандартная операция компилирования в ра-
бочий байт-код, после ÷åãî используется SQL-оператор CREATE FUNCTION.
При декларировании UDF аргументы передаются в SQL-запрос, ис-
пользуя Java-метод.
import COM.ibm.db2.app.*; public class sample extends UDF { public void test(String arg1, Blob arg2, String arg3, int result) {... } } arg1, arg2, arg3, и result объекты Java-классов, совместимые с SQL-типами.
В UDF, реализованных с помощью Java, нельзя использовать стан-
дартные потоки ввода-вывода (System.in, System.out, and System.err).
В обычной ситуации обращение к базе данных посредством встроен-
ных функций используется достаточно часто. DB2 имеет ряд встроен­ных SQL-операторов, предназначенных для ускорения работы UDF.
Обязательным условием работы с пользовательской функцией явля-
ется регистрация функции в базе данных. В версии 6.1 DB2 в SQL-опе­раторе CREATE FUNCTION используется специальный параметр PARAMETER STYLE JAVA, указывающий, что функция создана на Ja­va. DB2 имеет встроенную возможность работы с параллельным испол­нением UDF. Параметр ALLOW PARALLEL, указанный при SQL-опера­торе CREATE FUNCTION, предназначен для распараллеливания вы­полнения нескольких функций.
После создания UDF классы, содержащие пользовательские функ-
ции, можно поместить в jar-архив для дальнейшего использования. jar­архив можно прописать в файле свойств sqlj.properties, содержащий па­ру sqlj.install_jar= jar-архив. Для вызова созданной UDF можно воспо­льзоваться командной строкой DB2, CALL SQLJ. INSTALL_JAR, где
Часть 5. Работа с базами данных при помощи Java 287
указан путь к jar-архиву и сам класс пользовательской функции. Уда­ление пользовательской функции из базы данных происходит по тем же правилам:
CALL SQLJ.REMOVE_JAR('myProc_jar')
Java-клиентские приложения, написанные с использованием SQLJ
или JDBC, могут вызывать UDFs. Необходимо наличие в системе трех компонентов для возможности использования пользовательских функ­ций (UDF).
Входные параметры определяемой пользователем функции переда-
ются в клиентскую программу в месте с выходящими параметрами. По­сле написания клиентской программы, содержащей UDF, ее необходимо отранслировать, поместить в созданный байт-код, в случае с Java, ис­полняемый класс — в каталог, доступный серверу базы данных.
Большие объекты, пользовательские типы данных и пользователь-
ские функции каждое само по себе представляет мощное средство. Но действительную мощность СУБД приобретает при совместном исполь­зовании этих средств.
Глава 40. Сравнение SQLJ и JDBC
Бизнес-логика Web-приложений основана на активном использова-
нии базы данных.
Стандартный набор API по работе с базами данных JDBC, прекрасно
подходит для работы с различными СУБД. Использование JDBC пред­назначено для создания динамических SQL-запросов, т. е. в тех случа­ях, когда выбор SQL-оператора зависит от программы, и реализован­ных на языке Java. JDBC использует не только различные драйверы для доступа к различным СУБД, но и набор API для обслуживания SQL-запросов.
Очень важной техникой, используемой в Web-приложениях, являет-
ся объединение связей (connection pooling), реализованная на Applicati­on Server. Когда логика приложения, используемая в клиентской сес­сии, нуждается в данных, размещенных в базе данных, в пуле остаются значения предыдущих значений соединения. И если данный пользова­тель уже имел контакт с базой данных, то из пула выбираются значе­ния его последнего успешного соединения.
SQLJ более широко использует упрощенный синтаксис по сравнению
с JDBC. SQLJ-транслятор генерирует статические SQL-запросы, .sqlj использует для доступа встроенные объекты, в то время как в JDBC­приложении инициализируются специальные классы.
JDBC API позволяет написать Java-программу с динамическими
SQL-вызовами. Обычно JDBC-программы выполняют SQL-операторы с привилегиями пользователя, загрузившего программу. SQLJ-программы выполняют SQL-операторы с привилегиями пользователя, создавшего данный пакет.
288 Часть 5. Работа с базами данных при помощи Java
С выходом новой версии основного набора API Java, называемого в
народе Java 2 и появлением расширений J2EE уровня предприятий, компания SUN анонсировала выход новой версии спецификации для доступа к базам данных JDBC. Данная спецификация расширена и име­ет ряд дополнительных возможностей по работе с СУБД.
Компания IBM поставляет только один пакет по расширению
SQLJ — COM.ibm.db2.sqlj, содержащий набор классов по локализации SQLJ-сообщений.
Соответствие типов SQL типам Java приведено в таблице:
SQL Column Type Java Type (UDF) Java Type (Stored Procedure)
SMALLINT short short
INTEGER int int
BIGINT long long
FLOAT double double
REAL float float
DECIMAL java.math.BigDecimal java.math.BigDecimal
NUMERIC java.math.BigDecimal java.math.BigDecimal
CHAR(n) String String
CHAR(n) FOR BIT DATA Blob Blob
C null-terminated string String String
VARCHAR(n) String String
VARCHAR(n) FOR BIT Blob Blob
LONG VARCHAR String String
LONG VARCHAR FOR BIT DATA_ Blob Blob
GRAPHIC(n) String String
C null-terminated graphic string String String
VARGRAPHIC(n) String String
LONG VARGRAPHIC String String
BLOB(n)( Blob Blob
CLOB(n) Clob Clob
DBCLOB(n) Clob Clob
DATE String String
TIME String String
TIMESTAMP TIMESTAMP TIMESTAMP
Использование SQLJ имеет ряд преимуществ перед использованием
JDBC.
Во-первых, для каждого встроенного SQL-оператора в SQLJ-прило-
жении создается свой собственный скомпилированный оператор. Этот оператор применяется исключительно в одной точке программы. В при-
Часть 5. Работа с базами данных при помощи Java 289
ложении, использующем JDBC, каждый SQL-оператор идентифициру­ется по имени. Имя оператора позволяет другим частям программы об­ращаться к данному оператору.
Во-вторых, SQLJ использует статические операторы, известные уже
на момент написания SQLJ-приложения, а вот если эти операторы не­известны, то необходимо воспользоваться JDBC-технологией.
В-третьих, при организации соединения с базой данных JDBC рабо-
тает со значениями, передаваемыми в программу, SQLJ работает со значениями по умолчанию. Это значит, что сама программа будет забо­титься о верности данных, обеспечивающих соединение с рабочей базой данных. Объект класса Connection Context содержит все параметры со­единения.
В-четвертых, встроенные SQL-операторы в обычном SQLJ-приложе-
íèè обрабатываются перед исполнением программы специальным транслятором. Этот этап гарантирует еще одну проверку как правиль­ности используемых SQL-операторов, так и их обращение к базе дан­ных.
В-пятых, везде, где возможно, во встроенных операторах использо-
вание констант можно свободно заменить на переменные Java, исполь­зуемые в клиентском приложении.
В-шестых, Java-переменным можно присваивать полученные в
результате запроса значения. Полученные значения впоследствии обра­батываются прикладной программой.
В SQLJ-приложениях не требуется никакого приведения типов, в от-
личие от JDBC, где каждую строку результата программист должен яв­но привести к определенному типу. Также все null значения SQLJ-при­ложение обрабатывает само.
Далее. В SQLJ не требуется дополнительного кода для работы с
внутренним ресурсами, такими, как операторы освобождения или полу­ченного результата
Также в SQLJ-приложениях не требуется применения оператора
сложения (+) для использования в длинных SQL-строках. При исполь­зовании JDBC, приложение при каждом новом соединении с базой дан­ных создает новый объект.
Использование метода close() закрытия сеанса связи происходит для
JDBC- и SQLJ-приложений по-разному. В SQLJ-приложениях метод close() принадлежит только объекту соединения — ConectionContext. В момент вызова метода close() все ассоциированные с ним соединения закроются. В JDBC произойдет полная очистка ресурсов «собирателем мусора». Это значит, что в случае необходимости необходимо будет за­ново переустанавливать соединение.
Использование статических операторов позволит избежать предва-
рительной подготовки приложения для обращения к базам данных. Ес­ли же применение динамических операторов неизбежно, то существует несколько достаточно легких способов ускорить выполнение SQL-запро­са. Можно использовать статические операторы проведения транзакций SQL ROLLBACK и COMMIT вместо динамических. Прочие операторы, кроме SELECT, ROLLBACK и COMMIT, выполняются с помощью
290 Часть 5. Работа с базами данных при помощи Java
EXECUTE IMMEDIATE, а не с помощью стандартных последовательно­стей PREPARE и EXECUTE.
Наконец, SQLJ позволяет напрямую работать с переменными языка
Java.
Очень многие системные архитекторы пеняют на относительную
медлительность исполнения Java-программ при обращении к базам данных. Но основная проблема здесь кроется в древнем подходе при со­здании таблиц базы данных. Еще на заре создания СУБД одним из основных постулатов было определение для каждой сущности базы данных отдельной таблицы. Но было красивое решение для старых воз­можностей. Когда-то и автор был молодым и красивым. На сегодняшний момент требования к скорости передачи и доступу к данным увеличи­лись в несколько десятков раз, а требования распределенных систем, таких, как Web, стали напрямую зависеть от скоростных характери­стик. И если раньше для клиента создавалась отдельная таблица со всеми свойственными атрибутами — имя, адрес, количество жен и про­чая очень важная информация, — то теперь важнее получить всего один-два параметра первичной регистрации, а затем уже работать с другими данными о пользователе. Создание простой таблицы максимум с тремя полями — Login, password и foreign key — из основной табли­цы, описывающей пользователя системы, без использования преслову­того, вездесущего ID, очень любимого системным архитекторами. Испо­льзование такой маленькой таблички, лишенной всех дополнительных атрибутов информации, имеет несколько атрибутов. Во-первых, при ре­гистрации в системе эту таблицу можно расположить в определенной области памяти. Во-вторых, назначив полю login значение unique, мож­но еще на один шаг обезопасить систему. Другой шаг обеспечения без­опасности заключается в предоставлении определенных правил по ра­боте с данной таблицей, нежели предоставлять для каждого поля опре­деленные права доступа. В-четвертых, данную таблицу очень легко модифицировать. Все это применимо не только к обычной регистрации пользователя в системе, но и к использованию идентификационных карт, дебетовых и кредитных или других регистрационных механизмов.
К чему весь этот длинный монолог? Java не так медлительна, как хо-
телось бы многим системным архитекторам, использующим в повсед­невной практике VB. Хорошо спроектированная база данных, а значит, и корректный SQL-запрос украсит не только Java-приложение, но и VB-приложение или даже PHP-приложения.
Для синхронной передачи, коей является регистрация пользователя,
ускорить процесс можно именно использованием SQLJ. Один маленький запрос — и проверка на условии равенства! Вот все. Если же, конечно, попробовать использование JDBC для столь ничтожной операции, то можно заставить Java работать очень медленно. Автор не против JDBC, автор за то, чтоб, у каждой женщины был свой муж. Для каждой кон­кретной цели нужны свои способы достижения. Хорошо, когда их, спо­собов, много, и плохо, когда используется всего один, и то не очень точ­но. После чего начинаются всякого рода непонимания.