Добавление Doctrine в устаревшую базу данных, not null поля соединения вызывают нарушение ограничения целостности

#php #mysql #doctrine-orm

#php #mysql #доктрина-orm

Вопрос:

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

 CREATE TABLE `users` (
  `user_id` bigint(20) NOT NULL,
  `created` datetime NOT NULL,
  `org_id` bigint(20) NOT NULL,
   PRIMARY KEY  (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `orgs` (
  `org_id` bigint(20) NOT NULL,
  PRIMARY KEY  (`org_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 

Затем объект, соответствующий этой таблице:

 /**
 * @ORMEntity
 * @ORMTable(name="users")
 */
class User {
    /**
     * @ORMId
     * @ORMColumn(type="bigint")
     * @ORMGeneratedValue
     */
    protected $user_id;

    /**
     * @ORMColumn(type="datetime")
     */
    protected $created;

    /**
     * @ORMManyToOne(targetEntity="Org")
     * @ORMJoinColumn(name="org_id", referencedColumnName="org_id")
     */
    protected $org;
}

/**
 * @ORMEntity
 * @TORMable(name="orgs")
 */
class Org {
    /**
     * @ORMId
     * @ORMColumn(type="bigint")
     * @ORMGeneratedValue
     */
    protected $org_id;
}
 

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

 {
  "user_id": 1,
  "org_id": 0
}
 

Однако при попытке вставить новую строку:

 $user = new User;
$em->persist($user);
$em->flush();
 

Появится сообщение об ошибке:

 PDO Exception: An exception occurred while executing 'INSERT INTO users (created, org_id) VALUES (?, ?)' with params ["2020-12-11 14:41:27", null]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'org_id' cannot be null
 

Таким образом , кажется , что там , где Доктрина видит отсутствие связи , она вставляет null . Поэтому я могу решить эту проблему, изменив org_id столбец на NULL вместо NOT NULL . Однако:

  1. это означает, что будут строки с 0 и строки, с null которыми оба указывают на отсутствие связи
  2. существующее приложение, использующее эту таблицу, ожидает int возврата, что означает, что потребуется переработать много кода (не говоря уже о запросах, которые будут обрабатывать null и 0 совсем по-разному)

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

Комментарии:

1. Является @ID ли (заглавная буква) преднамеренной?

2. Есть ли в вашей таблице orgs организация с идентификатором 0? Если нет, то дурачиться с заменой нулей на ноль не поможет. Я предполагаю, что попытка добавить «0 org» может вызвать собственный набор проблем с другим кодом. Легко создавать базы данных, с которыми Doctrine не может справиться. Замена нулей и нулей — одна из них.

3. @Ocramius я не уверен; проверяя документы, я вижу, что они используют @Id , так что, возможно, это опечатка. Будет ли это что-нибудь нарушать?

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

5. @Ocramius Теперь я обновил @ID свои модели @Id , но, насколько я вижу, это, похоже, ни на что не повлияло

Ответ №1:

Попробуйте

 $default = $em->find('Org', 0);

$user = new User;
$user->setDefaultOrg($default);
$user->created = new DateTime();
$em->persist($user);
$em->flush();
 

Затем в вашем классе User

 use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 * @ORMHasLifecycleCallbacks
 * @ORMTable(name="users")
 */
class User {

  protected $default_org;

  /**
   * @ORMID
   * @ORMColumn(type="bigint")
   * @ORMGeneratedValue
   */
  protected $user_id;

  /**
   * @ORMColumn(type="datetime")
   */ 
  public $created;

  /**
   * @ORMManyToOne(targetEntity="Org", cascade={"persist"})
   * @ORMJoinColumn(name="org_id", referencedColumnName="org_id")
   */
  protected $org;

   /**
    * @ORMPrePersist()
    */
   public function fixLegacyOrg()
   {
      if (is_null($this->org)) {
        $this->org = $this->default_org;
      }
   }

  public function setDefaultOrg(Org $org)
  {
      $this->default_org = $org;
  }
}
 

Зависит от ваших потребностей в тестировании, которые вы могли бы ввести в конструктор (в идеале только для организации, а не для всего менеджера сущностей)

в качестве дополнительного примечания, чтобы это сработало, вам нужно добавить АвтоИнкремент в запрос таблицы сверху (я полагаю, это ошибка копирования / вставки)

Комментарии:

1. Спасибо, я проверю это. Я предполагаю, что мне придется связать сущности с entity manager, но это неплохая цена за возможность использовать Doctrine без перепрограммирования всего приложения и базы данных.

2. к сожалению, это не работает, поскольку find() метод возвращает null из-за того, что организация с идентификатором 0 не существует

3. Я пытался использовать другие прослушиватели, такие как PreFlush и postPersist, чтобы принудительно присвоить org_id значение 0, но, похоже, оно перезаписывается null где-то внутри единицы работы. Пытаюсь выяснить, где, чтобы я мог вручную переопределить его.

Ответ №2:

Я решил эту проблему, но я предполагаю, что это решение может расстроить некоторых людей. Я предварю это следующим: это решение для случаев, когда вы абсолютно не можете обновить свою схему, чтобы использовать поля с нулевым значением в ваших отношениях с внешним ключом. Например, в моем случае есть сотни мест, где используются объекты базы данных, где ожидается, что «пустое» поле соединения будет int , а не nullable int .

В этом случае решение, которое работает и определенно противоречит рекомендациям Doctrine, заключается в предварительной установке «пустых» версий ваших объектов в диспетчере объектов перед выполнением любых запросов:

 /** @var EntityManager $em **/
$uow = $em->getUnitOfWork();
$uow->createEntity(Org::class, ['org_id' => 0]);
 

Для каждого случая, когда у вас есть это поле «0 вместо nullable» в соединении, вы можете добавить это в Entity Manager. Вероятно, в ваших моделях также требуется обрабатывать это с нулевыми значениями. Например, в User

 public function getOrg(): ?Org{
  $org = $this->org;
  // Expected null condition
  if (!$org){
    return null;
  }
  
  // UoW hacked "null" entity
  if (!$org->org_id){
    return null;
  }

  return $org;
}
 

Этот механизм позволяет избежать утечки entity manager непосредственно в модели, но добавляет шум внутри моделей, которые должны проверять наличие этих «обнуляемых» объектов. Я не уверен, как это работает в отношении более сложных запросов DDL, но это помогает использовать Doctrine, и я бы посоветовал любому, кто его использует, запускать отдельные проекты для постепенного удаления каждого поддельного объекта, делая его поле внешнего ключа обнуляемым и модифицируя приложение для поддержки этого.