Как исправить 'Невозможно найти запись.Ключ не указан '? - PullRequest
1 голос
/ 02 апреля 2019

Я использую сервер Firebird 2.5 для записи в файл базы данных (BD.fbd).Мой проект Delphi XE8 имеет модуль данных (DMDados) с:

  • SQLConnection (conexao)

  • TSQLQUery1 (QueryBDPortico_Inicial) + TDataSetProvider1 (DSP_BDPortico_Inicial) + TClientDataSet1 (cdsBDPortico_Inicial)

  • TSQLQUery2 (QueryConsulta) (только для использования строк SQL)

Файл моей базы данных содержит эту таблицу:

  • PORTICO_INICIAL

В таблице есть следующие поля (все целые числа):

  • NPORTICO

  • ELEMENTO

  • ID

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

Мой код, запускаемый кнопкой, должен удалить записи всех таблиц (если они есть), а затем заполнить таблицу целыми числами, созданными LOOP.В первой попытке код просто работает нормально, но когда я нажимаю кнопку во второй раз, я получаю сообщение об ошибке «Невозможно найти запись.Ключ не указан 'тогда, когда я проверяю записи, таблица пуста.

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

    procedure monta_portico ();
    var
    I,K,L,M, : integer;
    begin
    with DMDados do
      begin
        QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;';
        QueryConsulta.ExecSQL();
        K := 1;
        for I := 1 to 10 do 
          begin
          L := I*100;
          for M := 1 to 3 do
            begin
              cdsBDPortico_Inicial.Insert;
              cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger := 
                M+L;
              cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
              cdsBDPortico_Inicial.ApplyUpdates(0);
              K := K +1;
            end;
          end;
      end;
    end;

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

Ответы [ 3 ]

3 голосов
/ 02 апреля 2019

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

Сообщение об ошибке Unable to find record. No key specifiedгенерируется DataSetProvider, поэтому не подключается напрямую к вашему

QUeryCOnsulta.SQL.Text := 'DELETE FROM PORTICO_INICIAL;'

, потому что это обходит DataSetProvider.Ошибка происходит из-за неудачной попытки к ApplyUpdates на CDS.Попробуйте изменить свой вызов на

Assert(cdsBDPortico_Inicial.ApplyUpdates(0) = 0);

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

Youскажем,

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

Если это так, когда возникает проблема, это потому, что вы сталкиваетесь с фундаментальным ограничением работы DataSetProvider.Чтобы применить обновления к исходному набору данных, он должен сгенерировать SQL для отправки обратно на исходный набор данных (TSqlQuery1), который уникально идентифицирует строку для обновления в исходных данных, что невозможно, если исходный набор данных содержитдублированные строки.

По сути, вам нужно переосмыслить свой код, чтобы все строки набора исходных данных были уникальными.Как только вы это сделаете, установка DSP UpdateMode на upWhereAll должна избежать проблемы.Конечно, было бы лучше, если бы исходный набор данных имел первичный ключ.

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

Кстати, извините, что поднял вопрос о ProviderFlags.Это не имеет значения, если есть дублированные строки, потому что независимо от того, на что они установлены, DSP все равно не сможет обновить одну запись.

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

function RowExists(ADataset : TDataSet; FieldNames : String; Values : Variant) : Boolean;
begin
  Result := ADataSet.Locate(FieldNames, Values, []);
end;

procedure TForm1.PopulateTable;
var
  Int1,
  Int2,
  Int3 : Integer;
  i : Integer;
  RowData : Variant;
begin
  CDS1.IndexFieldNames := 'Int1;Int2';
  for i := 1 to 100 do begin
    Int1 := Round(Random(100));
    Int2 := Round(Random(100));
    RowData := VarArrayOf([Int1, Int2]);
    if not RowExists(CDS1, 'Int1;Int2', RowData) then
      CDS1.InsertRecord([Int1, Int2]);
  end;
  CDS1.First;
  Assert(CDS1.ApplyUpdates(0) = 0);
end;
1 голос
/ 02 апреля 2019

Разделите проблему на небольшие группы, используя функции и процедуры, создайте экземпляр TSqlQuery. Выполните SQL-статистику и уничтожьте экземпляр, когда закончите с ним ...

procedure DeleteAll;
var
  Qry: TSqlQuery;
begin
  Qry := TSqlQuery.Create(nil);
  try
    Qry.SqlConnection := DMDados.conexao;
    Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL;';
    Qry.ExecSql;
  finally
    Qry.Free;
  end;
end;

. Вы даже можете выполнить непосредственно из TSQlConnection.с одной строкой ...


DMDados.conexao.ExecuteDirect('DELETE FROM PORTICO_INICIAL;')

procedure monta_portico ();
var
I,K,L,M, : integer;
begin
with DMDados do
  begin

    DeleteAll;

    K := 1;
    for I := 1 to 10 do
      begin
      L := I*100;
      for M := 1 to 3 do
        begin
          cdsBDPortico_Inicial.Insert;
          cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger :=
            M+L;
          cdsBDPortico_Inicial.FieldbyName('ELEMENTO').AsInteger := M;
          cdsBDPortico_Inicial.ApplyUpdates(0);
          K := K +1;
        end;
      end;
  end;
end;
0 голосов
/ 04 апреля 2019

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

cdsBDPortico_Inicial.FieldbyName('NPORTICO').AsInteger := 

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

cdsBDPortico_Inicial.ApplyUpdates(0);

Опять же, применение обновлений относительно медленное - для этого требуется обратное обращение к серверу по всем внутренним внутренностям библиотеки DataSnap.почему так часто?

Кстати, вы удаляете строки из таблицы SQL - но где вы удаляете строки из cdsBDPortico_Inicial ???Я не вижу этот код.

Если бы я был на ваших шоу, я бы написал что-то подобное (если я не большой поклонник Datasnap и CDS):

procedure monta_portico ();
var
  Qry: TSqlQuery;
  _p_EL, _p_NP: TParam;
  Tra: TDBXTransaction; 
var
I,K,L,M, : integer;
begin
  Tra := nil;
  Qry := TSqlQuery.Create(DMDados.conexao); // this way the query would have owner
  try   // thus even if I screw and forget to free it - someone eventually would

    Qry.SqlConnection := DMDados.conexao;
    Tra := Qry.SqlConnection.BeginTransaction;

    // think about making a special function that would create query
    // and set some its properties - like connection, transaction, preparation, etc
    // so you would not repeat yourself again and again, risking mistyping

    Qry.Sql.Text := 'DELETE FROM PORTICO_INICIAL'; // you do not need ';' for one statement, it is not script, not a PSQL block here
    Qry.ExecSql;

    Qry.Sql.Text := 'INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) '
                  + 'VALUES (:NP,:EL)';
    Qry.Prepared := True;
    _p_EL := Qry.ParamByName('EL'); // cache objects, do not repeat linear searches
    _p_NP := Qry.ParamByName('NP'); // for simple queries you can even do ... := Qry.Params[0]

    K := 1;
    for I := 1 to 10 do
      begin
      L := I*100;
      for M := 1 to 3 do
        begin
          _p_NP.AsInteger := M+L;
          _p_EL.AsInteger := M;
          Qry.ExecSQL;
          Inc(K); // why? you seem to never use it 
        end;
      end;

    Qry.SqlConnection.CommitFreeAndNil(tra);
  finally
    if nil <> tra then Qry.SqlConnection.RollbackFreeAndNil(tra);

    Qry.Destroy;
  end;
end;

Эта процедура незаселите cdsBDPortico_Inicial - а вам это действительно нужно?Если вы это сделаете - возможно, вы сможете перечитать его из базы данных: могут быть и другие программы, которые также добавили строки в таблицу.Или вы можете вставить много строк и затем применить их все в одной команде, прежде чем совершать транзакцию (часто сокращается до tx), но даже тогда не вызывайте FieldByName более одного раза.

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

Кроме того, если вы сделаете сервер Firebird автоматически назначающим поле ID (и ваша программа не требует специальныхзначения в ID и будут в порядке со значениями, созданными Firebird), тогда следующая команда может подойти вам еще лучше: INSERT INTO PORTICO_INICIAL(NPORTICO,ELEMENTO) VALUES (:NP,:EL) RETURNING ID

...