Очень часто хочется создать из ничего объект, сохранить его на сервере и стразу же получить с сервера все автоматически генерируемые поля. Например, 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, Вы писали:
IT>Т.е. ты решил это сделать отдельным методом?
Да. Теоретически возможен вариант, когда нужно и IDataReader получить и выходные параметры обработать.
Насчёт того, чтобы добавить ещё ExecuteObject(), который сам вызовет MapOutputParameters я тоже додумался — это самый распространённый use case, его имеет смысл оформить красиво.
Для 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
Здравствуйте, Блудов Павел, Вы писали:
БП>Для DataAccessorBuilder пока что реализован только самый простой вариант, т.е. не трубующий дополнительных атрибутов.
А как использовать возвращаемые параметры Оракла? Или это еще не реализовано?
Здравствуйте, 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
SELECTPersonSeq.CURVAL PersonID
FROM DUAL;
Здравствуйте, 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);
Но чёткого видения картины мира у меня пока нет, а городить полдюжины атрибутов, которые потом очень захочется заменить чем-то более осмысленным не хочется.
Луше позапрягать подольше, но зато потом ездить с комфортом.
Здравствуйте, Блудов Павел, Вы писали:
БП>Здравствуйте, Andy77, Вы писали:
A>>Но ведь так фактически получится два запроса к серверу (ведь возвращается курсор), или я ошибаюсь? БП>Нисколько.
А как же твои слова про out-параметры "И это будет выполняться гораздо быстрее, чем предыдущий способ, так как параметры уже вернулись с сервера на выходе из ExecuteNonQuery, а чтение из DataReader'а в предыдущем примере требует ещё одного обращения к серверу"? Ведь в качестве "медленного" варианта была приведена ХП, аналог которой ты сейчас привел на PL/SQL?
БП>Правда, ODP немного жульничает в ExecuteScalar, но всё равно вариант с возвращаемыми параметрами быстрее. Причем чем дальше сервер, тем быстрее возвращаемые параметры.
Как это? Относительно времени, затраченного на вызов ХП, или в абсолютных величинах?
БП>Пока что приходит в голову только такая вот реализация: БП>
public abstract Person DoSmth([Out("PersonID"), InOut("LastName", "MiddleName"), NoMap("Gender")] Person e);
Выглядит не очень красиво, конечно. А не получится объявить все IDBParameters как InOut и после выполнения ХП замапить обратно все параметры?
БП>Луше позапрягать подольше, но зато потом ездить с комфортом.
Здравствуйте, 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. Тут можно нарваться на очень неочевидное затирание полей с одинаковыми именами.
Так что лучше явно, но один раз прописать, что именно должен делать генерируемый метод. Чем меньше неочевидных действий, тем лучше.
Здравствуйте, Блудов Павел, Вы писали:
БП>Я имел в виду: "нисколько не ошибаешься". Прошу прощения за невнятность.
Спасибо, теперь всё встало на свои места, а то я уже думал, что у меня с головой что-то не то творится
БП>Как минимум, не эффективно.
Мне кажется, что это экономия на спичках по сравнению с временем выполнения/возврата ХП (а это, по аналогии, будет покупка мотоцикла
БП>В 99% случаев нужно замапить обратно первичный ключ и всё. Остальное экзотика.
Согласен. Только хочется это сделать за одно путешествие на сервер Мне бы первичного ключа из out-параметров хватило бы с головой, но ведь ты правильно заметил про "позапрягать подольше и ездить с комфортом". Вот и пытаюсь помочь в мозговом штурме в надежде уменьшить время "запрягания"
БП>Кроме того, могут быть ещё такие извраты: БП>
public abstract Person DoSmth(string FirstName, Person e);
БП>т.е. FirstName сам по себе, а всё остальное берётся из Person. Тут можно нарваться на очень неочевидное затирание полей с одинаковыми именами.
Если ХП изменяет значение каких-то полей, значит, так и нужно. Ведь "случайно" этого произойти не может. Эта ХП будет вызываться только этим методом, так что всё остаётся во власти программиста, никаких неоднозначностей.
БП>Так что лучше явно, но один раз прописать, что именно должен делать генерируемый метод. Чем меньше неочевидных действий, тем лучше.
Больно уж длинная запись получается, да и передавать имена полей как строки — надежный способ установки граблей.
Здравствуйте, 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 метод атрибутов не напасёшься.
Здравствуйте, Блудов Павел, Вы писали:
A>>Больно уж длинная запись получается, да и передавать имена полей как строки — надежный способ установки граблей. БП>Хм.. А, что, Есть варианты
Ну да
public abstract Person DoSmth([InOut] Person e);
или же для любителей извращений вариант, минимизирующий кол-во встречающихся в строках имен полей и по-прежнему предоставляющий полную свободу действий
public abstract Person DoSmth(outstring firstName, [Out, In("firstName")] Person e);
БП>У меня пока что есть только идея для CRUDL Insert. Вот тут можно сделать отдельный атрибут, который укажет, что это не только первичный ключ, но и ещё и генерируемый на стороне сервера. Т.е. что-то типа:
Здравствуйте, Блудов Павел, Вы писали:
БП>Здравствуйте, 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);
}
Здравствуйте, Andy77, Вы писали:
A>Да, забыл сказать, это всё происходит с System.Data.OracleClient.
Тогда всё понятно. ODP провайдер добавляет префикс "p", а Oracle provider нет. Подправьте или провайдера или процедуру.
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 за один приём.
БП>Очень часто хочется создать из ничего объект, сохранить его на сервере и стразу же получить с сервера все автоматически генерируемые поля. Например, identities.
БП>Примерно вот так: БП>
Person p = new Person("John", "Pupkin");
БП>SomeMagicToInsertIntoTablePerson(p);
БП>// Вот здесь p.ID уже имеет осмысленное и готовое к употреблению значение.
БП>
Ребят, а как в результате решился этот вопрос?
Тулкит юзаю уже год, но с патченой процедурой, которая тупо возвращает SCOPE_IDENTITY. Есть уже какие-то нормальные пути получения этого автоинкрементного поля (только без каких-либо stored procedures) ?
Спасибо!
Re[2]: Добавление записи и получение Primary Key за один при
Здравствуйте, matumba, Вы писали:
M>Ребят, а как в результате решился этот вопрос? M>Тулкит юзаю уже год, но с патченой процедурой, которая тупо возвращает SCOPE_IDENTITY. Есть уже какие-то нормальные пути получения этого автоинкрементного поля (только без каких-либо stored procedures) ? M>Спасибо!
ну так так и есть пока ничего другого не придумали... вроде )
Re[3]: Добавление записи и получение Primary Key за один при
Здравствуйте, ili, Вы писали:
ili>Здравствуйте, matumba, Вы писали:
M>>Ребят, а как в результате решился этот вопрос? ili>ну так так и есть пока ничего другого не придумали... вроде )
Ну "так" — это как? Опять дурацкие сторед процедуры?