Место нарушения null что это значит

от admin

NULL-значения в PostgreSQL: правила и исключения

Навскидку многим кажется, что они знакомы с поведением NULL-значений в PostgreSQL, однако иногда неопределённые значения преподносят сюрпризы. Мы с коллегами написали статью на основе моего доклада с PGConf.Russia 2022 — он был полностью посвящён особенностям обработки NULL-значений в Postgres.

NULL простыми словами

Что такое SQL база данных? Согласно одному из определений, это просто набор взаимосвязанных таблиц. А что такое NULL? Обратимся к простому бытовому примеру: все мы задаём друг другу дежурный вопрос: «Как дела?». Часто мы получаем в ответ: «Да ничего. » Вот это «ничего» нам и нужно положить в базу данных — NULL: неопределённое, некорректное или неизвестное значение.

Допустим, вы суммируете две колонки, и в сотой по счёту записи наткнулись на NULL. Что тогда делать? Или возвращать ошибку, потому что так нельзя, или всё-таки как-то выполнить сложение и идти дальше. Сообщество решило в пользу второго варианта и закрепило это в стандартах языка SQL. Также договорились, что данные любого типа могут оказаться NULL, написали специальные функции и операции для обработки NULL-значений.

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

Какие значения не являются NULL?

Давайте теперь посмотрим, что не есть NULL. Ноль — это просто ноль, не NULL. Пустая строка — это пустая строка в Postgres, в отличие от Oracle. Пустой массив, пустой JSON, массив NULL-значений, пустой диапазон — это не NULL. Сложные типы, включающие NULL, уже не являются NULL.

Есть, правда, одно исключение: запись, собранная из NULL-значений, является NULL. Это сделано для совместимости со стандартом языка SQL. Однако, «под капотом» Postgres функции и операторы считают запись, состоящую из NULL-значений, NOT NULL. Ниже приведены результаты обработки такой записи для некоторых из них:

сount(row(NULL)) посчитает такую запись;

num_nulls(row(NULL)) выдаст ноль;

row(NULL) IS DISTINCT FROM NULL выдаст TRUE.

Ещё удивительнее пример с записями, содержащими NULL:

row(NULL::int, ‘Bob’::TEXT) IS NULL ожидаемо выдаст FALSE, но

row(NULL::int, ‘Bob’::TEXT) IS NOT NULL тоже выдаст FALSE!

Тем не менее, это поведение не является багом и описано в документации.

Операции с NULL

Почти все бинарные операции с NULL — сложить, вычесть, умножить, конкатенировать — дают на выходе NULL. С этим стоит быть осторожнее. Если вы к строке или к JSON конкатенируете что-то, оказавшееся NULL, то получаете на выходе NULL. А если вы ещё и сделали UPDATE в базу данных, выйдет совсем нехорошо.

Тем не менее, логическая операция TRUE OR NULL на выходе даёт TRUE. FALSE AND NULL даёт в результате FALSE. То есть существуют некоторые исключения из общего правила.

Операции сравнения

Операции сравнения — больше, меньше, больше или равно — c NULL на выходе дают NULL. При этом и сам NULL не равен самому себе. Впрочем, в PostgreSQL есть параметр transform_null_equals, который по умолчанию выключен. Если его включить, то NULL будет равен NULL.

Для проверки любого значения на NULL в Postgres предусмотрен специальный оператор — … IS NULL, . IS NOT NULL. Также может быть непривычно, что при сравнении булевых переменных с NULL или при применении оператора равенства помимо значений TRUE и FALSE возможно ещё и неизвестное значение. При этом оператор IS (NOT) UNKNOWN — это аналог IS (NOT) NULL для булевых переменных.

Операторы IS TRUE или IS FALSE для булевых переменных дают или TRUE, или FALSE. NULL в результате их применения получиться не может. Использование оператора IS TRUE позволяет писать более надёжный код, чем обычное сравнение = TRUE, которое может выдать не учтённое программистом NULL-значение и пойти «не туда».

Что если нам нужно сравнить два значения X и Y, считая, что NULL-значения равны друг другу? Можно самому написать конструкцию из логических операторов, но существует уже готовый оператор X IS (NOT) DISTINCT FROM Y. Правда, планировщик PostgreSQL плохо понимает этот оператор и может выдавать долгие планы выполнения для запросов с ним.

Cпециальные функции для работы с NULL

Обратимся к специальным функциям для работы с NULL. Всем известная coalesce возвращает первый NOT NULL аргумент. Есть nullif, есть num_nulls — этой функции можно дать сколько угодно аргументов, она посчитает количество NULL-значений. С помощью функции num_nonnulls можно посчитать NOT NULL значения.

Как правило, функции с произвольным числом аргументов игнорируют NULL. Такие функции, как greatest, concat его просто проигнорируют. При этом функция создания массивов включит NULL-значение во вновь образованный массив, за этим надо следить.

NULL и агрегатные функции

Что касается агрегатных функций, то array_agg, json_agg включают NULL в агрегат, а конкатенация строки не может вставить NULL-значение в середину строки, и поэтому она NULL игнорирует.

Статистические функции min, max, sum игнорируют NULL, а вот с выражением Count всё хитро. Count по конкретному полю посчитает только строки, где выражение NOT NULL, а вот Count со звёздочкой посчитает всё, включая NULL-значения.

Что со всем этим делать? Можно почитать в справке или потестировать, как функция обрабатывает NULL-значения. А лучше использовать выражение FILTER и в явном виде исключить все NULL-значения.

NULL и пользовательские функции

Теперь о пользовательских функциях. При создании пользовательской функции по умолчанию включен режим CALLED ON NULL INPUT, то есть при наличии NULL среди аргументов функция вызовется и будет обрабатывать это значение. Если вам это не нужно, можно использовать RETURNS NULL ON NULL INPUT либо STRICT — в этом случае функция, обнаружив NULL хотя бы в одном аргументе, сразу возвращает NULL и дальше вообще не думает — для экономии времени.

Многие системные функции в PostgreSQL определены именно как STRICT, поэтому стали возможны некоторые математические казусы. Например, NULL можно разделить на ноль, и в результате вы получите NULL — вместо ошибки деления на ноль. NULL в степени ноль тоже является NULL, хотя в математике любое число в нулевой степени, даже если это сам ноль, даёт единицу. Непонятно, правильно ли такое поведение с философской точки зрения, но вроде пока никто не жаловался.

Группировка и сортировка

Если говорить о группировке, то она считает все NULL-значения одинаковыми, так как это делает оператор IS NOT DISTINCT FROM. При сортировке есть специальные подвыражения, в которых можно указать NULLS FIRST или NULLS LAST. По умолчанию выбирается NULLS LAST, то есть считается, что неопределённые значения больше всех остальных чисел.

Сортировка работает так при создании выборки, индекса, в агрегатных функциях и оконных функциях.

NULL и записи

Когда мы формируем запись из нескольких значений, то сравниваются все NOT NULL значения. Если найдётся различие, то результат будет FALSE. Если все NOT NULL значения совпадают, и нашёлся NULL, то будет NULL.

Сравнение на больше/меньше выполняется по другим правилам. Как только попадётся не совпадающее значение, тогда оно будет больше или меньше, а если обнаружится NULL, то будет NULL.

NULL и диапазоны

С бинарными операциями разобрались, но что если у нас тернарная операция? Например, SELECT NOW BETWEEN NULL AND NULL. Получится, ожидаемо, NULL.

Однако, точно такое же выражение, сформулированное через диапазоны, неожиданно даёт TRUE. Да, с точки зрения Postgres здесь и сейчас мы находимся в неопределённом промежутке времени!

Согласно стандарту SQL, все диапазонные типы — не только временные, а все вообще —воспринимают границу NULL как бесконечность.

Я полюбопытствовал и выяснил, что промежуток от минус бесконечности до плюс бесконечности входит в промежуток от NULL до NULL, а обратное — неверно.

Выходит, что NULL здесь даже несколько больше, чем бесконечность.

Также я попытался проверить, входит ли NULL в промежуток от минус бесконечности до плюс бесконечности. Оказалось, что это неизвестно. Это контринтуитивный для меня момент: мне казалось, что полный диапазон значений от минус до плюс бесконечности должен включать в себя любое значение, в том числе и неопределённое. Но нет, в PostgreSQL это не так.

Откуда в запросах появляются NULL-значения?

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

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

В-третьих, NULL-значения могут появляться в результате операции объединения LEFT JOIN.

В-четвёртых, NULL-значения появлются как результат некоторых функций при некоторых условиях.

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

Структура базы данных и NULL

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

При определении ограничения (constraint) тоже есть одна особенность: если условие возвращает NULL, это считается допустимым, и такая запись может быть вставлена.

Например, ограничение Foreign key позволяет в дочерней таблице вставить запись со ссылкой, которая является NULL. Это будет допустимо.

Ограничение CHECK (price > 0) даст вам вставить в таблицу поле для Price со значением, равным NULL.

Ограничение unique позволяет создать несколько записей со значением NULL. Правда, в PostgreSQL 14 уже появилось специальное «заклинание», которое может запретить несколько записей с NULL.

Как NULL хранится внутри записи БД?

NULL вообще не хранится среди полей записи, но если там есть хотя бы одно NULL-значение, то создаётся битовая карта неопределённых значений, которая называется t_bits. Стоит запомнить, что самое первое NULL-значение влечёт за собой создание такой карты и расход некоторого количества места.

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

NULL и индексы

Postgres хранит NULL-значения в btree-индексах. Этим он отличается от Oracle. Также Postgres может использовать такой индекс при поиске записей по NULL-значению.

Тем не менее, хранение NULL-значений в индексе для вас бесполезно, если у вас нет такого типа запросов (они довольно редки, и их можно проверить в представлении pg_stats_statements).

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

Вот пример случая с большим числом NULL-значений в таблице. У вас есть внешний ключ (foreign key) на какую-то родительскую таблицу, но реальная ссылка используется редко, и в основном в дочерней таблице NULL-значения.

Или же у вас может быть какой-то хитрый функциональный индекс, который часто возвращает NULL. Здесь у нас пример по JSONB-ключу key1, а если у вас в JSON этот ключ встречается нечасто, то и большинство значений будет NULL.

Если у вас NULL-значений много, то вам поможет перестроение индекса на частичный с условием WHERE <ваше поле или выражение> IS NOT NULL. То есть мы просто выкидываем такие значения из нашего индекса. Это принесёт ряд улучшений:

во-первых, сокращается размер индекса на дисках, в том числе на репликах и бэкапах;

во-вторых, уменьшится количество записей в журнал предзаписи (WAL);

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

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

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

Сценарий аккуратного переезда вполне очевиден:

создаёте новый частичный индекс;

по представлению pg_stat_user_indexes убеждаетесь, что запросы переехали на новый индекс;

удаляете старый индекс.

Выводы

Значение NULL может преподнести некоторые сюрпризы, если вы к нему не готовы.

Стоит проверить, как работают с NULL вызываемые вами функции и ваш код.

Запрещайте NULL там, где вы не планируете его использовать явным образом.

Проверяйте ваши индексы на наличие NULL-значений — возможно, за счёт оптимизаций удастся сэкономить некоторое количество памяти и ресурсов процессора.

Полезные ссылки

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

Классическая статья Брюса Момжиана (Bruce Momjian) под названием «NULLs Make Things Easier?» доступна здесь.

Русские Блоги

В Гонконге действительно, Null, кажется, является проблемой головной боли повсюду, такой как NullPointerException, которая является головной болью в Java. Чтобы избежать неожиданного исключения нулевого указателя, программные обезьяны были осторожны в коде для различных суждений тысячелетий. Это раздутый, поэтому java8 представил Optional, чтобы избежать этой проблемы.

Следующая вещь, о которой мы хотим поговорить, — это null в MySQL. В большом количестве статей и книг по оптимизации MySQL упоминается, что поля должны использовать как можно больше NOT NULL, а не NULL, если только нет особых обстоятельств. Тем не менее, они только дают заключение, не объясняя причину, точно так же, как куриный суп не дает ложку, поэтому многие новички наполовину сомневаются или сомневаются в этом заключении. В этой статье подробно анализируются причины использования Null сегодня и приводятся некоторые причины, по которым не следует использовать Null.

1. Почему NULL используется многими людьми?

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

Многие думают, что для нуля не нужно больше места, но это не главное.

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

2. Это ложное обвинение?

Документы официального сайта MySQL:

NULL columns require additional space in the rowto record whether their values are NULL. For MyISAM tables, each NULL columntakes one bit extra, rounded up to the nearest byte.

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

—— Из «Высокопроизводительного MySQL Second Edition»

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

3. Дайте мне причину не использовать Null?

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

(2) Обновления с NULL до не-NULL не могут быть обновлены на месте, и, скорее всего, произойдет разбиение индекса, что влияет на производительность.

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

(3) Значения NULL склонны к проблемам в соответствии с типом отметки времени, особенно если параметр initial_defaults_for_timestamp не включен

(4)NOT IN,! = И другие запросы с отрицательными условиями возвращают всегда пустые результаты, когда значение NULL, запрос подвержен ошибкам

Учимся избегать null-значений в современном Java. Часть 1

Java

В своем выступлении “Null References: The billion dollar mistake” (“Нулевые ссылки: ошибка на миллиард долларов”), Тони Хоар описывает реализацию нулевых ссылок в языках программирования ALGOL, что также по его словам стало ошибкой стоимостью в миллиард долларов. Такие авторитетные книги, как Clean Code: A Handbook of Agile Software Craftsmanship (“Чистый код: настольное руководство по гибкой разработке ПО”) рекомендуют использовать нуль как можно реже. В то же время в книге Bug Patterns in Java (“Шаблоны ошибок в Java”) проблемам, связанным с нулевыми значениями, посвящается аж целых три главы. Тема “What is a null pointer exception and how do I fix it” (“Что такое исключение нулевого значения и как его исправить”), обсуждаемая на Stack Overflow, набрала уже более 3 млн просмотров. Работа с нулевыми значениями и впрямь может вызвать немало сложностей.

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

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

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

Чем опасен null?

Null — это особое значение, поскольку оно не ассоциируется ни с каким типом (можете смело проверить это инструкцией instanceof в отношении любого другого класса в JRE) и с радостью занимает место любого другого объекта в присвоениях переменных и вызовах методов. Именно в этом и кроются две основных его опасности:

  1. Каждое возвращаемое значение сложного типа может быть null.
  2. Каждое значение параметра со сложным типом может также быть null.
Читать:
Какие целые числа следуют за числами

В результате любое возвращаемое значение или объект параметра — это потенциальное исключение нулевого указателя (NPE), возникающее в случае неправильной обработки.

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

Может тогда вообще никогда не присваивать значениям null? Тоже неудачное предположение. Если учесть тот факт, что в каждом языке программирования есть пустое значение ( nil, undefined, None, void и т.д.), то наличие общего значения, обозначающего отсутствие чего-либо, чрезвычайно полезно.

Ошибка в условии цикла while может породить бесконечный цикл в любой программе, но это не делает такие циклы плохими по природе. Аналогично будет неверным считать, что null всегда неуместен только из-за того, что его неправильное использование может привести к ошибкам. Null — это наиболее естественное значение для выражения конкретных вещей, но при этом очень неподходящее для выражения других. Компетентные разработчики должны уметь различать эти случаи. В следующем разделе я как раз перейду к объяснению этого.

Когда Null уместен, а когда нет

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

Возвращение null

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

Сейчас я работаю над проектом электронной записи пациентов Columna, в котором у нас есть классы, представляющие элементы рабочего процесса больницы вроде пациентов, медикаментов, врачей, больничных отделений, госпитализаций и пр. При моделировании любой области возникают случаи, когда нам нужно допустить для определенного элемента отсутствие значения. Предположим, что у нас есть класс, представляющий госпитализацию с атрибутами, которые ее описывают: больничное отделение, куда помещается пациент, причина госпитализации, ее время и т.д. Аналогичным образом у нас может быть класс, который представляет пациента с набором атрибутов вроде имени и номера социального страхования. В любой момент времени пациент может быть госпитализирован или нет. Говоря более формально, у нас есть связь типа “имеет” с мощностью 0..1.

Представим метод, извлекающий из базы данных информацию о госпитализации данного пациента:

Что должен возвращать этот метод для не госпитализированного пациента, если не null? Есть ли для выражения этого более точное значение? Спорю, что нет.

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

Некоторые выступают за использование так называемого шаблона проектирования Null Object вместо null. Главная его идея в реализации пустого класса с минимумом или вообще без функциональности, т.е. нулевого объекта, который можно использовать вместо класса, содержащего действительную функциональность. Лучше всего данный шаблон показывает себя в сценариях, где нужно рекурсивно обойти структуру бинарного дерева в поиске суммы значений всех узлов. Для этого вы выполняете поиск в глубину и рекурсивно суммируете в каждом узле значения его левого и правого поддерева.

Дерево поиска обычно реализуется так, что каждый узел в нем имеет левого и правого потомка, которые являются либо также узлами, либо концевыми вершинами. Если такое представление дерева использует для концевых узлов null, то вам придется явно выполнять проверки на нулевых потомков, чтобы останавливать рекурсию в концевом узле, предотвращая попытку получения его значения. Вместо этого вам следует определить интерфейс Node с простым методом getValue() и реализовать его в представляющем узел классе, который вычисляет значение, складывая значения getValues() потомков, как показано на рисунке ниже. Реализуйте такой же интерфейс в классе, представляющем узел, и пусть класс концевого узла возвращает при вызове 0. Теперь нам больше не нужно различать код между концевым узлом и обычным. Необходимость явно проверять наличие null отпала вместе с риском получения исключения (NPE).

Применение шаблона нулевого объекта к структуре дерева

Для применения этого шаблона к примеру с госпитализацией нам потребуется создать интерфейс Admission . Затем мы определим класс AdmissionImpl для случаев, когда мы можем вернуть данные фактической госпитализации и класс AdmissionNullObjectImpl для случаев, когда не можем. Это позволит методу getAdmission() возвращать либо реальный AdmissionImpl , либо AdmissionNullObjectImpl . Так как вызывающий код использует общий тип Admission, мы можем рассматривать оба объекта одинаково, не рискуя получить исключение и не загромождая код проверками обработки null.

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

Что будет возвращать класс AdmissionNullObject , когда в нем нет данных? Что он должен возвращать вместо объекта локации, когда вызывается getLocation() ? Какой будет подходящая начальная дата для возврата?

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

Еще одна альтернатива — это использовать вместо null пустую string , если атрибутом является простая строка. Это избавляет нас от риска получить NPE, но при этом для правильной обработки пустого значения, скорее всего, потребуется столько же проверок, как и в случае с null. Кроме того, семантика будет отличаться: пустая строка представляет строку с пустым значением, а null не представляет ничего. Это становится актуальным, когда приложению нужно различать, ввел пользователь информацию в виде значения пустой строки или нет. Вы избавляетесь от риска получить NPE ценой применения несколько сбивающего с толку значения.

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

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

Такие объекты де факто не должны быть представлены нулевым значением. Не только в нашей базе кода, но также и в JRE, библиотеках и фреймворках. Это можно назвать проблематичным, но вопрос насколько? Важно понимать, что несмотря на необходимость минимизировать использование null, мы не должны добиваться этого ценой переполнения баз кода сложными обходными маневрами с целью избежать появления таких значений. Как я уже отмечал, альтернативы в данном случае не всегда хороши. Однако есть случаи возвращения null, которых избежать легко, хотя встречаются они частенько. Возвращение null для классов в таких ситуациях вызывает подозрение. Например, иногда null возвращается из методов геттеров, где объект не может быть создан по определенным причинам вроде ошибки сервера при его вызове методом. Этот метод обрабатывает ошибку, регистрируя ее в инструкцию catch , и вместо создания экземпляра объекта возвращает null. Это легко исправить. Исключение должно использоваться для указания на неполадку и привлекать для ее обработки вызывающий код. Возвращение null в подобных сценариях будет сбивать с толку, не обеспечит обработку ошибки и может перенести проблему в другую часть кода, где было бы лучше использовать систему fail-fast, немедленно останавливающую работу приложения в случае потенциального сбоя.

Еще одно ошибочное использование null — это представление связи “имеет” с мощностью 0..* (в начале статьи я говорил о связях 0..1). Если вернуться к примеру с объектом пациента, то в нем пациент может иметь одного/нескольких зарегистрированных родственников или не иметь их совсем. Тем не менее я часто вижу, что люди возвращают null, когда для заполнения списка или других типов коллекций нет данных. Аналогичным образом null в качестве аргумента используется в методах для замещения отсутствующих коллекций. Но его применение в данном случае нежелательно по ряду причин. Он сбивает с толку, поскольку коллекцией мощность связи представляется идеально, а присвоив null типу коллекции, вы только добавляете ненужные риски в код. Цикл for , основанный на коллекции, ничего не делает, если эта коллекция пуста. В противном же случае он перебирает каждый элемент, выполняя определенные действия. Если вы позволите коллекции быть null, то выразите, по сути, пустой список, означающий, что в нем обрабатывать нечего. Однако при этом вам придется обеспечить выполнение проверки на null для каждого последующего метода, использующего эту коллекцию, иначе может возникнуть NPE. В случаях, когда нет данных для представления — вызывайте методы с пустыми коллекциями. Это также легко, как вызвать Collections.emptyList() , emptyMap() , emptySet() и т.д. Ненамного больше работы, чем объявить null, зато намного лучше.

Передача нулевых параметров

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

Как же полностью избежать нулевых параметров?

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

В таких языках, как Python, сигнатуры методов могут содержать предустановленные значения параметров, используемые при отсутствии значения аргумента в вызове метода. Тем не менее в Java такое невозможно. Ближайшим аналогом этого будет использовать перегрузку метода, когда в классе одна и та же сигнатура метода определяется несколько раз с разными параметрами. Один метод будет содержать всю функциональность и принимать весь набор параметров, а другие будут просто “декораторами” для вызова этого метода, каждый из которых будет получать свой поднабор параметров. Методы-декораторы определяют, какие значения должны использовать вместо отсутствующих параметров, чтобы вызывающему компоненту не пришлось их предоставлять. Жестко прописывая, какие значения должны предоставляться, когда у вызывающего их не хватает, мы уменьшаем риск появления ошибок и делаем принимаемые значения параметров явными.

Аналогичным образом можно разбирать конструкторы, но также можно использовать шаблон строитель. Он помогает минимизировать число параметров конструктора и удаляет необходимость передавать в него нулевые значения, предоставляя для создания класса объект Builder . Смысл данного шаблона в косвенном инстанцировании объекта через промежуточный класс builder . Для предоставления аргументов, которые вы могли бы передать в конструктор напрямую, вы вызываете соответствующий каждому сеттер. Если значение еще не было установлено, builder предоставит его. Затем вы вызываете для builder метод Create() , и он инстанцирует объект за вас. Как и в большинстве шаблонов, в строителе вводятся дополнительные классы и сложность, поэтому прежде, чем его использовать, убедитесь, что в этом есть смысл. Использование его только ради избежания вызова конструктора с парой нулевых значения, скорее всего, будет излишним.

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

Воспринимайте null правильно

Все больше языков программирования начинают реализовывать определенные возможности с учетом безопасности. Например, в таких языках, как Clojure, F# и Rust переменные по умолчанию неизменяемы. Компилятор допускает изменение значений только для тех из них, которые объявлены со специальным модификатором. Такой способ использования опасных функций вынуждает программистов переопределять поведение по умолчанию, указывая тем самым, что они осознают степень риска и делают это не без весомых оснований. И к null нам стоит относиться аналогичным образом. Нужно придерживать это значение для особых случаев, где оно будет вполне уместно, ограничив при этом его использование в целом, опять же не ценой усложнения кода креативными обходными решениями. При каждом намерении использовать null вместо перемещающегося между методами значения следует учесть оправданность этого. В таком случае вы должны гарантировать, что в итоге оно не окажется в том месте, где может вызвать проблемы, и другие разработчики будут знать, что значение может быть null. Если же этого обеспечить нельзя, то лучше рассмотреть другие варианты.

NULL-значения в SQL

Достаточно часто встречаются такие случаи, когда в таблице имеются записи с не заданными значениями какого-либо из полей, потому что значение поля неизвестно или его просто нет. В таких случаях SQL позволяет указать в поле NULL-значение. Строго говоря, NULL-значение вовсе не представлено в поле. Когда значение поля есть NULL — это значит, что программа базы данных специальным образом помечает поле, как не содержащее какого-либо значения для данной строки (записи).

Дело обстоит не так в случае простого приписывания полю значения «нуль» или «пробел», которые база данных трактует как любое другое значение. Поскольку NULL не является значением как таковым, он не имеет типа данных. NULL может размещаться в поле любого типа. Тем не менее, NULL, как NULL-значение, часто используется в SQL.

Предположим, появился покупатель, которому еще не назначен продавец. Чтобы констатировать этот факт, нужно ввести значение NULL в поле snum, а реальное значение включить туда позже, когда данному покупателю будет назначен продавец.

SQL IS NULL

Поскольку NULL фиксирует пропущенные значения, результат любого сравнения при наличии NULL-значений неизвестен. Когда NULL-значение сравнивается с любым значением, даже с NULL-значением, результат просто неизвестен. Булево значение «неизвестно» ведет себя также, как «ложь» — строка, на которой предикат принимает значение «неизвестно», не включается в результат запроса – при одном важном исключении: NOT от лжи есть истина (NOT (false)=true), тогда как NOT от неизвестного значения есть также неизвестное значение. Следовательно, такое выражение как «city = NULL» или «city IN (NULL)» является неизвестным независимо от значения city.

Часто необходимо различать false и unknown – строки, содержащие значения столбца, не удовлетворяющие предикату, и строки, которые содержат NULL. Для этой цели SQL располагает специальным оператором IS, который используется с ключевым словом NULL для локализации NULL-значения. SQL IS NULL. Пример. Вывести все поля из талицы Customers, значения поля city которых равны NULL:

Похожие статьи