Добавление записи и получение Primary Key за один приём.
От: Блудов Павел Россия  
Дата: 11.08.06 11:46
Оценка:

Введение

Очень часто хочется создать из ничего объект, сохранить его на сервере и стразу же получить с сервера все автоматически генерируемые поля. Например, identities.

Примерно вот так:
Person p = new Person("John", "Pupkin");
SomeMagicToInsertIntoTablePerson(p);
// Вот здесь p.ID уже имеет осмысленное и готовое к употреблению значение.


Реализация на низком уровне

На низком уровне всё просто. Можно, например написать вот такую хранимую процедуру (MsSql):
CREATE Procedure Person_Insert
    @FirstName  nvarchar(50),
    @LastName   nvarchar(50),
    @MiddleName nvarchar(50),
    @Gender     char(1)
AS

INSERT INTO Person
    ( LastName,  FirstName,  MiddleName,  Gender)
VALUES
    (@LastName, @FirstName, @MiddleName, @Gender)

SELECT Cast(SCOPE_IDENTITY() as int) PersonID

и вызывать её таким вот образом:
using (DbManager db = new DbManager())
{
    db
        .SetSpCommand("Person_Insert", db.CreateParameters(e))
        .ExecuteObject(e);
}

Т.е. с объекта типа Person создаются параметры, вызывается хранимая процедура, а возвращаемые поля (в данном случае PersonID) автоматически мапятся на тот же самый объект. Этот подход хорош тем, что в таком виде хранимую процедуру можно легко заменить на простой запрос и использовать с базой данных не поддерживающей хранимые проуедуры (Access, SqlCe).

А можно воспользоваться возвращаемыми параметрами (Oracle):
CREATE OR REPLACE 
PROCEDURE Person_Insert
    ( pFirstName  IN NVARCHAR2
    , pLastName   IN NVARCHAR2
    , pMiddleName IN NVARCHAR2
    , pGender     IN CHAR
    , pPersonID   OUT NUMBER
    ) IS
BEGIN
INSERT INTO Person
    ( LastName,  FirstName,  MiddleName,  Gender)
VALUES
    (pLastName, pFirstName, pMiddleName, pGender)
RETURNING
    PersonID
INTO
    pPersonID;
END;

и вызывать её вот таким образом:
using (DbManager db = new DbManager())
{
    db
        .SetSpCommand("Person_Insert", db.CreateParameters(e, new string[]{"PersonID"}, null))
        .ExecuteNonQuery();
        
    db.MapOutputParameters(e);
}

И это будет выполняться гораздо быстрее, чем предыдущий способ, так как параметры уже вернулись с сервера на выходе из ExecuteNonQuery, а чтение из DataReader'а в предыдущем примере требует ещё одного обращения к серверу. Кроме того, инициализация чтения из DataReader'а в BLToolkit'е довольно сложная процедура, в то время как чтение значения из IDbDataParameter'а это простейшая операция.

Кроме того, есть ещё один, немного экзотический вариант. Если возвращается ровно одно значение, то его можно вернуть из хранимой процедуры в качестве RETURN_VALUE (MsSql):
CREATE FUNCTION Person_Insert
    @FirstName  nvarchar(50),
    @LastName   nvarchar(50),
    @MiddleName nvarchar(50),
    @Gender     char(1)
RETURNS int
AS

INSERT INTO Person
    ( LastName,  FirstName,  MiddleName,  Gender)
VALUES
    (@LastName, @FirstName, @MiddleName, @Gender)

RETURN Cast(SCOPE_IDENTITY() as int)

В этом случае использовать это можно так:
using (DbManager db = new DbManager())
{
    db
        .SetSpCommand("Person_Insert", db.CreateParameters(e))
        .ExecuteNonQuery();
        
    db.MapOutputParameters(e, "PersonID");
}


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

Реализация на высоком (DataAccessor) уровне

А здесь все весьма неочевидно. Если для DataAccessorBuider можно будет реализовать все эти варианты, введя нужное количество атрибутов, то в самом DataAccessor'е для CRUDL операций нужно что-то одно. Либо реализовывать их всех с разными суффиксами. Вобщем, полный
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re: Добавление записи и получение Primary Key за один приём.
От: IT Россия linq2db.com
Дата: 11.08.06 13:52
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

        
БП>    db.MapOutputParameters(e);


Т.е. ты решил это сделать отдельным методом?
Если нам не помогут, то мы тоже никого не пощадим.
Re[2]: Добавление записи и получение Primary Key за один при
От: Блудов Павел Россия  
Дата: 14.08.06 01:05
Оценка:
Здравствуйте, IT, Вы писали:

IT>Т.е. ты решил это сделать отдельным методом?

Да. Теоретически возможен вариант, когда нужно и IDataReader получить и выходные параметры обработать.

Насчёт того, чтобы добавить ещё ExecuteObject(), который сам вызовет MapOutputParameters я тоже додумался — это самый распространённый use case, его имеет смысл оформить красиво.
... << RSDN@Home 1.2.0 alpha rev. 642>>
DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 14.08.06 03:54
Оценка:

DataAccessorBuilder

Для DataAccessorBuilder пока что реализован только самый простой вариант, т.е. не трубующий дополнительных атрибутов.
Пример:
        public abstract class PersonAccessor : DataAccessor
        {
            public abstract Person Insert([Destination(NoMap = false)] Person e);
        }
        
        // ...
        
        public void InsertGetID()
        {
            Person    e = (Person)TypeAccessor.CreateInstance(typeof(Person));
            e.FirstName = "Crazy";
            e.LastName  = "Frog";
            e.Gender    = Gender.Other;

            _da.Insert(e);
            // Здесь уже e.ID > 0
        }


CREATE Procedure Person_Insert
    @FirstName  nvarchar(50),
    @LastName   nvarchar(50),
    @MiddleName nvarchar(50),
    @Gender     char(1)
AS

INSERT INTO Person
    ( LastName,  FirstName,  MiddleName,  Gender)
VALUES
    (@LastName, @FirstName, @MiddleName, @Gender)

SELECT Cast(SCOPE_IDENTITY() as int) PersonID
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 16.08.06 21:20
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

БП>Для DataAccessorBuilder пока что реализован только самый простой вариант, т.е. не трубующий дополнительных атрибутов.


А как использовать возвращаемые параметры Оракла? Или это еще не реализовано?
Re[2]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 17.08.06 02:04
Оценка: 4 (1)
Здравствуйте, Andy77, Вы писали:

A>А как использовать возвращаемые параметры Оракла?

Точно так же:

CREATE OR REPLACE 
    ( pFirstName  IN NVARCHAR2
    , pLastName   IN NVARCHAR2
    , pMiddleName IN NVARCHAR2
    , pGender     IN CHAR
    )
RETURN
    SYS_REFCURSOR
IS
    retCursor      SYS_REFCURSOR;
    lPersonID      NUMBER;
BEGIN

INSERT INTO Person( LastName,  FirstName,  MiddleName,  Gender)
VALUES            (pLastName, pFirstName, pMiddleName, pGender)
RETURNING   PersonID
INTO       lPersonID;

OPEN retCursor FOR
    SELECT lPersonID PersonID
    FROM   DUAL;
        
RETURN retCursor;
    
END;


Различия чисто синтаксические — для Oracle нужно явно объявить курсор и явно вернуть его. Остальное без изменений.
Как вариант, RETURNING ... INTO ... можно убрать, а SELECT заменить на
OPEN retCursor FOR
    SELECT PersonSeq.CURVAL PersonID
    FROM   DUAL;
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re[3]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 17.08.06 02:07
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

A>>А как использовать возвращаемые параметры Оракла?

БП>Точно так же:

Но ведь так фактически получится два запроса к серверу (ведь возвращается курсор), или я ошибаюсь?
Re[4]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 17.08.06 02:40
Оценка:
Здравствуйте, Andy77, Вы писали:

A>Но ведь так фактически получится два запроса к серверу (ведь возвращается курсор), или я ошибаюсь?

Нисколько. Правда, ODP немного жульничает в ExecuteScalar, но всё равно вариант с возвращаемыми параметрами быстрее. Причем чем дальше сервер, тем быстрее возвращаемые параметры.

Так что DataAccessorBuilder нужно доделывать, но есть объективные сложности.

Предположим, имеется вот такая сигнатура метода

public abstract Person DoSmth(Person e);


При этом хочется, чтобы Person.FirstName был Input, Person.LastName и MiddleName были InputOutput, Person.ID был Output, а Person.Gender вообще не учавствовал в мапинге параметров.

Пока что приходит в голову только такая вот реализация:
public abstract Person DoSmth([Out("PersonID"), InOut("LastName", "MiddleName"), NoMap("Gender")] Person e);


Но чёткого видения картины мира у меня пока нет, а городить полдюжины атрибутов, которые потом очень захочется заменить чем-то более осмысленным не хочется.
Луше позапрягать подольше, но зато потом ездить с комфортом.
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re[5]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 17.08.06 03:07
Оценка: 11 (1)
Здравствуйте, Блудов Павел, Вы писали:

БП>Здравствуйте, Andy77, Вы писали:


A>>Но ведь так фактически получится два запроса к серверу (ведь возвращается курсор), или я ошибаюсь?

БП>Нисколько.

А как же твои слова про out-параметры "И это будет выполняться гораздо быстрее, чем предыдущий способ, так как параметры уже вернулись с сервера на выходе из ExecuteNonQuery, а чтение из DataReader'а в предыдущем примере требует ещё одного обращения к серверу"? Ведь в качестве "медленного" варианта была приведена ХП, аналог которой ты сейчас привел на PL/SQL?

БП>Правда, ODP немного жульничает в ExecuteScalar, но всё равно вариант с возвращаемыми параметрами быстрее. Причем чем дальше сервер, тем быстрее возвращаемые параметры.


Как это? Относительно времени, затраченного на вызов ХП, или в абсолютных величинах?

БП>Пока что приходит в голову только такая вот реализация:

БП>
public abstract Person DoSmth([Out("PersonID"), InOut("LastName", "MiddleName"), NoMap("Gender")] Person e);


Выглядит не очень красиво, конечно. А не получится объявить все IDBParameters как InOut и после выполнения ХП замапить обратно все параметры?

БП>Луше позапрягать подольше, но зато потом ездить с комфортом.


Разумеется
Re[6]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 17.08.06 03:18
Оценка:
Здравствуйте, Andy77, Вы писали:

БП>>Нисколько.


A>А как же твои слова про out-параметры "И это будет выполняться гораздо быстрее, чем предыдущий способ, так как параметры уже вернулись с сервера на выходе из ExecuteNonQuery, а чтение из DataReader'а в предыдущем примере требует ещё одного обращения к серверу"? Ведь в качестве "медленного" варианта была приведена ХП, аналог которой ты сейчас привел на PL/SQL?

Я имел в виду: "нисколько не ошибаешься". Прошу прощения за невнятность.

БП>>Правда, ODP немного жульничает в ExecuteScalar.

A>Как это?
http://www.oracle.com/technology/tech/windows/odpnet/index.html

БП>>Пока что приходит в голову только такая вот реализация:

БП>>
public abstract Person DoSmth([Out("PersonID"), InOut("LastName", "MiddleName"), NoMap("Gender")] Person e);


A>Выглядит не очень красиво, конечно. А не получится объявить все IDBParameters как InOut и после выполнения ХП замапить обратно все параметры?


Как минимум, не эффективно. В 99% случаев нужно замапить обратно первичный ключ и всё. Остальное экзотика.
Кроме того, могут быть ещё такие извраты:
public abstract Person DoSmth(string FirstName, Person e);

т.е. FirstName сам по себе, а всё остальное берётся из Person. Тут можно нарваться на очень неочевидное затирание полей с одинаковыми именами.
Так что лучше явно, но один раз прописать, что именно должен делать генерируемый метод. Чем меньше неочевидных действий, тем лучше.
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re[7]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 17.08.06 03:56
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

БП>Я имел в виду: "нисколько не ошибаешься". Прошу прощения за невнятность.


Спасибо, теперь всё встало на свои места, а то я уже думал, что у меня с головой что-то не то творится

БП>Как минимум, не эффективно.


Мне кажется, что это экономия на спичках по сравнению с временем выполнения/возврата ХП (а это, по аналогии, будет покупка мотоцикла

БП>В 99% случаев нужно замапить обратно первичный ключ и всё. Остальное экзотика.


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

БП>Кроме того, могут быть ещё такие извраты:

БП>
public abstract Person DoSmth(string FirstName, Person e);

БП>т.е. FirstName сам по себе, а всё остальное берётся из Person. Тут можно нарваться на очень неочевидное затирание полей с одинаковыми именами.

Если ХП изменяет значение каких-то полей, значит, так и нужно. Ведь "случайно" этого произойти не может. Эта ХП будет вызываться только этим методом, так что всё остаётся во власти программиста, никаких неоднозначностей.

БП>Так что лучше явно, но один раз прописать, что именно должен делать генерируемый метод. Чем меньше неочевидных действий, тем лучше.


Больно уж длинная запись получается, да и передавать имена полей как строки — надежный способ установки граблей.
Re[8]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 17.08.06 05:00
Оценка:
Здравствуйте, Andy77, Вы писали:

БП>>Как минимум, не эффективно.



БП>>Кроме того, могут быть ещё такие извраты:

БП>>
public abstract Person DoSmth(string FirstName, Person e);

БП>>т.е. FirstName сам по себе, а всё остальное берётся из Person. Тут можно нарваться на очень неочевидное затирание полей с одинаковыми именами.

A>Если ХП изменяет значение каких-то полей, значит, так и нужно. Ведь "случайно" этого произойти не может. Эта ХП будет вызываться только этим методом, так что всё остаётся во власти программиста, никаких неоднозначностей.


Тут я имел в виду, что FirstName в списке параметров встречается дважды. Один раз отдельно, один раз как поле Person. И они могут быть разными.
Может произойти
FirstName->DbParameter->Person.FirstName
а это явно не то, что требовалось. Иначе можно было сделать FirstName->Person.FirstName ещё на клиентской стороне. Аргументы типа "ну это уже изврат" не принимаются.

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

Так что вариант с
public abstract Person DoSmth([InOut] Person e);

рассматриваем, но не циклимся.

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

Хм.. А, что, Есть варианты

У меня пока что есть только идея для CRUDL Insert. Вот тут можно сделать отдельный атрибут, который укажет, что это не только первичный ключ, но и ещё и генерируемый на стороне сервера. Т.е. что-то типа:
public class Person
{
  [PrimaryKey, ServerSide(ScalarSourceType.OutputParameter)]
  public int PersonID;

  [ServerSide(ScalarSourceType.OutputParameter)]
  public DateTime DateCreated;
    
    // ...
}


Но это, ещё раз повторяю, будет хорошо работать только случае CRUDL Insert. В более общем случае так сделать не получится.
В одном случае нужно будет чтобы PersonID был выходной параметр, в других входной и тому подобное.
На каждый generic метод атрибутов не напасёшься.
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re[9]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 17.08.06 05:31
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

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

БП>Хм.. А, что, Есть варианты

Ну да
public abstract Person DoSmth([InOut] Person e);

или же для любителей извращений вариант, минимизирующий кол-во встречающихся в строках имен полей и по-прежнему предоставляющий полную свободу действий
public abstract Person DoSmth(out string firstName, [Out, In("firstName")] Person e);




БП>У меня пока что есть только идея для CRUDL Insert. Вот тут можно сделать отдельный атрибут, который укажет, что это не только первичный ключ, но и ещё и генерируемый на стороне сервера. Т.е. что-то типа:


Я похожий подход уже вовсю у себя использую — http://rsdn.ru/Forum/?mid=2041623&amp;flat=0
Автор: Andy77
Дата: 04.08.06

Конечно же, да, используется только для Insert(Sql)
Re[3]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 22.08.06 16:33
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

БП>Здравствуйте, Andy77, Вы писали:


A>>А как использовать возвращаемые параметры Оракла?

БП>Точно так же:

Хм, как я ни бьюсь, получается вот такая бяка —

ORA-06550: line 1, column 32:
PLS-00306: wrong number or types of arguments in call to 'PERSON_INSERT'
ORA-06550: line 1, column 7:
PL/SQL: Statement ignored


При генерации списка параметров туда добавляется [Input]Personid, которого ХП совсем не ждет. Впрочем, если вообще убрать поле Personid из класса, то получим ту же самую ошибку...

FUNCTION PERSON_INSERT
    ( pFirstName  IN NVARCHAR2
    , pLastName   IN NVARCHAR2
    , pMiddleName IN NVARCHAR2
    , pGender     IN CHAR
    )
RETURN
    SYS_REFCURSOR
IS
    retCursor      SYS_REFCURSOR;
    lPersonID      NUMBER;
BEGIN

INSERT INTO Person( LastName,  FirstName,  MiddleName,  Gender)
VALUES            (pLastName, pFirstName, pMiddleName, pGender)
RETURNING   PersonID
INTO       lPersonID;

OPEN retCursor FOR
    SELECT lPersonID PersonID
    FROM   DUAL;

RETURN retCursor;

END;



   // auto-generated by MyGeneration.BLToolkit
   [TableName("MY.PERSON")]
   public class Person
   {
      [MapField("PERSONID"),
       PrimaryKey]                 public int Personid;
      [MapField("FIRSTNAME")]      public string Firstname;
      [MapField("LASTNAME")]       public string Lastname;
      [MapField("MIDDLENAME")]     public string Middlename;
      [MapField("GENDER")]         public string Gender;
   }

   public abstract class PersonAccessor : DataAccessor<Person>
   {
      public abstract Person Insert([Destination(NoMap = false)] Person e);
   }
Re[4]: DataAccessorBuilder case
От: Andy77 Ниоткуда  
Дата: 22.08.06 19:09
Оценка:
Здравствуйте, Andy77, Вы писали:

Да, забыл сказать, это всё происходит с System.Data.OracleClient.
OdpProvider, к сожалению, у меня не компилируется с ODP 9.2 Ненавижу Оракл.
Re[5]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 23.08.06 02:10
Оценка: 2 (1)
Здравствуйте, Andy77, Вы писали:

A>Да, забыл сказать, это всё происходит с System.Data.OracleClient.

Тогда всё понятно. ODP провайдер добавляет префикс "p", а Oracle provider нет. Подправьте или провайдера или процедуру.
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re[10]: DataAccessorBuilder case
От: Блудов Павел Россия  
Дата: 23.08.06 09:05
Оценка: 9 (1)
Здравствуйте, Andy77!

Доделал. Теперь можно так:

CREATE Procedure Person_Insert_OutputParameter
    @FirstName  nvarchar(50),
    @LastName   nvarchar(50),
    @MiddleName nvarchar(50),
    @Gender     char(1),
    @PersonID   int output
AS

INSERT INTO Person
    ( LastName,  FirstName,  MiddleName,  Gender)
VALUES
    (@LastName, @FirstName, @MiddleName, @Gender)

SET @PersonID = Cast(SCOPE_IDENTITY() as int)

GO

public abstract class PersonAccessor : DataAccessor
{
    public abstract void Insert_OutputParameter([Direction.Output("PersonID")] Person e);
    public abstract void Insert_ReturnParameter([Direction.ReturnValue("PersonID")] Person e);
}
// ...

Person    e = (Person)TypeAccessor.CreateInstance(typeof(Person));
e.FirstName = "Crazy";
e.LastName  = "Frog";
e.Gender    = Gender.Other;

_da.Insert_OutputParameter(e);
Assert.IsTrue(e.ID > 0);


В отличие от DesctinationAcctibute, Direction.XXXAttribute может быть несколько. Т.е.
    public abstract void Insert_OutputParameter([Direction.Output("PersonID", "HireDate")] Person e, [Direction.Output("OfficeID")] Office o, );


В этом случае возвращаемые параметры PersonID и HireDate замапятся на поля объекта типа Person, а OfficeID на поля объекта типа Office.

Осталось сделать ServerSideGeneratedAttribute чтобы Insert из CRUDL сам догадывался, что некоторые поля возвращаются обратно с сервера.
... << RSDN@Home 1.2.0 alpha rev. 642>>
Re: Добавление записи и получение Primary Key за один приём.
От: matumba  
Дата: 31.08.09 15:44
Оценка:
Здравствуйте, Блудов Павел, Вы писали:

БП>

Введение

БП>Очень часто хочется создать из ничего объект, сохранить его на сервере и стразу же получить с сервера все автоматически генерируемые поля. Например, identities.

БП>Примерно вот так:

БП>
Person p = new Person("John", "Pupkin");
БП>SomeMagicToInsertIntoTablePerson(p);
БП>// Вот здесь p.ID уже имеет осмысленное и готовое к употреблению значение.
БП>


Ребят, а как в результате решился этот вопрос?
Тулкит юзаю уже год, но с патченой процедурой, которая тупо возвращает SCOPE_IDENTITY. Есть уже какие-то нормальные пути получения этого автоинкрементного поля (только без каких-либо stored procedures) ?
Спасибо!
Re[2]: Добавление записи и получение Primary Key за один при
От: ili Россия  
Дата: 02.09.09 06:22
Оценка:
Здравствуйте, matumba, Вы писали:

M>Ребят, а как в результате решился этот вопрос?

M>Тулкит юзаю уже год, но с патченой процедурой, которая тупо возвращает SCOPE_IDENTITY. Есть уже какие-то нормальные пути получения этого автоинкрементного поля (только без каких-либо stored procedures) ?
M>Спасибо!

ну так так и есть пока ничего другого не придумали... вроде )
Re[3]: Добавление записи и получение Primary Key за один при
От: matumba  
Дата: 02.09.09 10:15
Оценка:
Здравствуйте, ili, Вы писали:

ili>Здравствуйте, matumba, Вы писали:


M>>Ребят, а как в результате решился этот вопрос?

ili>ну так так и есть пока ничего другого не придумали... вроде )

Ну "так" — это как? Опять дурацкие сторед процедуры?
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.