Какой генератор следует использовать в маппинге объекта,
если вставка объекта в таблицу реализуется через хранимую процедуру и там-же происходит генерация первичного ключа?
Здравствуйте, Нахлобуч, Вы писали:
Н>Здравствуйте, D.Triton, Вы писали:
DT>>Кто и как решал данную проблему?
Н>Просто мысль -- а если попробовать сделать
Н>exec up_User_i @ClientId=?, @Login=?, @FirstName=?, @LastName=?, @Email=?, @Password=?, @UserId=?; select scope_identity()
Думаю, нужно сделать так.
Сейчас сорцы хибернейта посмотрю (как работает генератор identity). И сделаю по его образу и подобию свой, только
чтобы давал на запуск кастомный запрос на добавление. Т.е итого мы получим запрос, приведенный вами выше.
Здравствуйте, Нахлобуч, Вы писали:
Н>Здравствуйте, D.Triton, Вы писали:
DT>>Кто и как решал данную проблему?
Н>Просто мысль -- а если попробовать сделать
Н>exec up_User_i @ClientId=?, @Login=?, @FirstName=?, @LastName=?, @Email=?, @Password=?, @UserId=?; select scope_identity()
Удалось добавить поддержку получения первичного ключа, в случае если добавление объекта в базу происходит через
хранимую процедуру.
Для этого понадобилось реализовать свой драйвер MsSql, свой Persister и IdentityGenerator
Даже, я бы сказал, работает поддержка out параметров. Синтаксис такой:
при маппинге на хп, NHibernate не умеет использовать ID который возвращается через out параметр.
ID в данном случае нужно возвращать select-ом из хп, тогда можно использовать <generator class="identity" />
--SELECT @ID
Пример:
CREATE PROCEDURE xsp_Ins_Person(
--<ParamDefBlock>
@ID bigint = null out
,@firstName varchar(254) = null
,@lastName varchar(254) = null
,@Debug bit = 0
--</ParamDefBlock>
)
AS
BEGIN
SET NOCOUNT ON--<DeclareBlock>
--<DeclareSysVarBlock>declare
@RET_CODE int
,@ERROR_CODE int
,@TRAN_CHECK int
--</DeclareSysVarBlock>
--<DeclareVarBlock>
--</DeclareVarBlock>
--</DeclareBlock>
--<PrepareTranBlock>set @TRAN_CHECK = case when @@TRANCOUNT > 0 or @@OPTIONS & 2 > 0 then 1 else 0 end
select
@RET_CODE = 0
,@ERROR_CODE = 0
if @TRAN_CHECK = 0 BEGIN TRAN--</PrepareTranBlock>
--<InsertBlock>INSERT INTO Person(firstName, lastName)
VALUES(@firstName, @lastName)
--</InsertBlock>set @ERROR_CODE = @@ERROR
if (@ERROR_CODE <> 0) or (@RET_CODE <> 0) GOTO UNDO
--<SuccBlock>
SUCC:
if (@TRAN_CHECK = 0) and (@@TRANCOUNT > 0) COMMIT TRAN
set @ID = SCOPE_IDENTITY()
SELECT @ID
RETURN(0)
--</SuccBlock>
--<UndoBlock>
UNDO:
if (@@TRANCOUNT > 0)
if @TRAN_CHECK = 0 ROLLBACK TRAN
RETURN(1)
--</UndoBlock>END
Здравствуйте, снежок, Вы писали:
С>при маппинге на хп, NHibernate не умеет использовать ID который возвращается через out параметр. С>ID в данном случае нужно возвращать select-ом из хп, тогда можно использовать <generator class="identity" />
Note that the custom sql-insert will not be used if you use identity to generate identifier values for the class.
Здравствуйте, Polevi, Вы писали: P>Note that the custom sql-insert will not be used if you use identity to generate identifier values for the class. P>?? P>не работает это
Какая версия? Я с NHibernate 2.0.0 Alpha2 прототипирую и тестирую. У меня работает, правда все равно в конечном итоге планирую guid использовать с генерацией на стороне приложения.
Здравствуйте, снежок, Вы писали:
С>Здравствуйте, Polevi, Вы писали: P>>Note that the custom sql-insert will not be used if you use identity to generate identifier values for the class. P>>?? P>>не работает это
С>Какая версия? Я с NHibernate 2.0.0 Alpha2 прототипирую и тестирую. У меня работает, правда все равно в конечном итоге планирую guid использовать с генерацией на стороне приложения.
1.2.1 у меня.. с альфой 2.0 решил пока не связываться
Здравствуйте, Polevi, Вы писали:
P>попробовал с 2.0 beta 1 P>SP вызывается, но генерируется вот такой sql P>exec sp_executesql N'exec ClientCreate @Id=@p0', N'@p0 int', @p0 = default P>такой запрос сервер не кушает — default ему непонятно
Была такая проблема, решилась путем описания в файле маппинга свойства ID, причем последним в списке свойств класса.
Тогда все ок.
Здравствуйте, снежок, Вы писали:
С>Здравствуйте, Polevi, Вы писали:
P>>попробовал с 2.0 beta 1 P>>SP вызывается, но генерируется вот такой sql P>>exec sp_executesql N'exec ClientCreate @Id=@p0', N'@p0 int', @p0 = default P>>такой запрос сервер не кушает — default ему непонятно
С>Была такая проблема, решилась путем описания в файле маппинга свойства ID, причем последним в списке свойств класса. С>Тогда все ок.
да так лучше
но теперь он после после вставки сразу пытается выполнить запрос на изменение.. и вываливается с ошибкой "не могу изменять идентити поле"
exec sp_executesql N'exec ClientCreate @Id=@p0', N'@p0 int', @p0 = 0
exec sp_executesql N'UPDATE Client SET Id = @p0 WHERE Id = @p1', N'@p0 int,@p1 int', @p0 = 44, @p1 = 44
вообщем сплошные пляски с бубном, видимо тоже буду смотреть в сторону GUID
Вижу уже не первый вопрос по маппингу с хранимками.
Никак не пойму зачем использовать хранимые процедуры если используется NHibernate и затем мужественно бороться с побочными эффектами возникающими при этом.
Я могу предположить только 3 варианта: натягиваем ORM на существующую и работающую схему БД сильно завязанную на ХП.
ХП кроме вставки делает что-то в других таблицах не замапленых NH.
ХП вставляет не то, что мы передали, делая какие-то хитрые БЛ преобразования.
Уважительной я считаю только первую причину. Во втором случае лучше сделать триггер, третьего лучше вообще не допускать.
Может есть причины которые я не увидел? Хочу понять, для чего это делается.
Здравствуйте, Ziaw, Вы писали:
Z>Вижу уже не первый вопрос по маппингу с хранимками. Z>Никак не пойму зачем использовать хранимые процедуры если используется NHibernate и затем мужественно бороться с побочными эффектами возникающими при этом.
Z>Может есть причины которые я не увидел? Хочу понять, для чего это делается.
Философская и флеймогонная тема.
Могу лишь высказать свои суждения вдополнение к выше приведенным по поводу использования хп, которые вполне могут быть оспорены (только не будем это делать здесь, ладно? .
1. По моему мнению с БД нужно работать аккуратно, потому выделяю CRUD в виде хп.
2. хп и вью дают некоторый уровень абстракции работы с БД.
3. Маппинг непосредственно на таблицы "связывает" руки разработчику БД. Т.е. не получается абстрагироваться от приложения, не получается жестко разделить роли. Разработчику/администратору БД приходится более тесно общаться с другими членами комманды, он теперь обязан знать маппинг NH и оценивать свои процедуры рефакторинга с оглядкой на маппинг.
4. DBA раздражает каша, которую он видит в профайлере. Ему трудно анализировать возникающие проблемы.
5. Часто существуют жесткие требования по безопасности, когда закрывают доступ к таблицам и дают доступ только к хп и вью.
6. Нет хп — нет возможности по-нормальному(самостоятельно) провести тестирование создания объектов БД, например, с помощью DbUnit, т.е. опять же без оглядки на код среднего звена/клиента.
P>да так лучше P>но теперь он после после вставки сразу пытается выполнить запрос на изменение.. и вываливается с ошибкой "не могу изменять идентити поле"
P>да так лучше P>но теперь он после после вставки сразу пытается выполнить запрос на изменение.. и вываливается с ошибкой "не могу изменять идентити поле"
P>exec sp_executesql N'exec ClientCreate @Id=@p0', N'@p0 int', @p0 = 0 P>exec sp_executesql N'UPDATE Client SET Id = @p0 WHERE Id = @p1', N'@p0 int,@p1 int', @p0 = 44, @p1 = 44
P>вообщем сплошные пляски с бубном, видимо тоже буду смотреть в сторону GUID
Покажите маппинг-файл.
Скорее всего в пред. версиях эта проблема нерешаемая.
...
GUID лучше — не будет впоследствии проблем с репликацией, бек-офисом и т.п.
Здравствуйте, снежок, Вы писали:
С>Здравствуйте, Polevi, Вы писали:
P>>попробовал с 2.0 beta 1 P>>SP вызывается, но генерируется вот такой sql P>>exec sp_executesql N'exec ClientCreate @Id=@p0', N'@p0 int', @p0 = default P>>такой запрос сервер не кушает — default ему непонятно
С>Была такая проблема, решилась путем описания в файле маппинга свойства ID, причем последним в списке свойств класса. С>Тогда все ок.
Для себя же в своем драйвере ( я использую хибернейт 1.2.1) пришлось выкручиваться следующим образом:
1. Наследоваться от SqlClientDriver
2. Перекрыть
public virtual IDbCommand GenerateCommand(CommandType type, SqlString sqlString, SqlType[] parameterTypes);
Т.к. ее текущая реализация не создавала output парамерты. Все создаваемые IDbDataParameter параметры имели Detination — Input.
Поэтому я и создал свой драйвер и перекрыл соотв. метод:
public class MySqlClientDriver : global::NHibernate.Driver.SqlClientDriver
{
private readonly string parameterPairPattern = @"@[^,]*";
public override IDbCommand GenerateCommand(CommandType type, SqlString sqlString, SqlType[] parameterTypes)
{
IDbCommand command = base.GenerateCommand(type, sqlString, parameterTypes);
string cmdSql = command.CommandText.ToLowerInvariant().Trim();
///Works only for stored procsif (cmdSql.StartsWith("exec", StringComparison.InvariantCulture))
{
cmdSql = cmdSql.Remove(0, 4).Trim();
Regex regex = new Regex(parameterPairPattern);
MatchCollection coll = regex.Matches(cmdSql);
for (int i = 0; i < coll.Count; ++i)
{
Match match = coll[i];
if (match.Value.EndsWith("out") || match.Value.EndsWith("output"))
{
((IDbDataParameter) command.Parameters[i]).Direction = ParameterDirection.Output;
}
}
}
return command;
}
/// <summary>
/// Checks whether identifier is generated by output parameter or not
/// </summary>
/// <param name="sql"></param>
/// <returns></returns>public virtual bool IsIdentifierGeneratedByOutParameter(SqlCommandInfo sql)
{
string cmdSql = sql.Text.ToString().ToLowerInvariant().Trim();
///Works only for stored procsif (cmdSql.StartsWith("exec", StringComparison.InvariantCulture))
{
cmdSql = cmdSql.Remove(0, 4).Trim();
Regex regex = new Regex(parameterPairPattern);
MatchCollection coll = regex.Matches(cmdSql);
if (coll.Count == 0)
return false;
Match match = coll[coll.Count - 1]; ///Identifier matchreturn match.Value.EndsWith("out") || match.Value.EndsWith("output");
}
return false;
}
}
Потом пришлось реализовать персистер.
По сути за основу взял AbstractPersister.
Только в реализацию метода
/// <summary>
/// Persist an object, using a natively generated identifier
/// </summary>protected object Insert(object[] fields, bool[] notNull, SqlCommandInfo sql, object obj,
ISessionImplementor session);
внес небольшие изменения (отмечено жирным курсивом):
/// <summary>
/// Persist an object, using a natively generated identifier
/// </summary>protected object Insert(object[] fields, bool[] notNull, SqlCommandInfo sql, object obj,
ISessionImplementor session)
{
if (log.IsDebugEnabled)
{
log.Debug("Inserting entity: " + ClassName + " (native id)");
if (IsVersioned)
{
log.Debug("Version: " + Versioning.GetVersion(fields, this));
}
}
try
{
bool useOutputParameterForId = false;
SqlString insertSelectSQL = null;
if (sql.CommandType == CommandType.Text)
{
MySqlClientDriver driver = session.SessionFactory.ConnectionProvider.Driver as MySqlClientDriver;
if (driver != null && driver.IsIdentifierGeneratedByOutParameter(sql))
{
useOutputParameterForId = true;
}
else
{
insertSelectSQL =
Dialect.AddIdentitySelectToInsert(sql.Text, GetKeyColumns(0)[0], GetTableName(0));
}
}
if (insertSelectSQL != null)
{
// Use one statement to insert the row and get the generated id
IDbCommand insertSelect =
session.Batcher.PrepareCommand(CommandType.Text, insertSelectSQL, sql.ParameterTypes);
IDataReader rs = null;
try
{
// Well, it's always the first table to dehydrate, so pass 0 as the position
Dehydrate(null, fields, notNull, propertyColumnInsertable, 0, insertSelect, session, 0);
rs = session.Batcher.ExecuteReader(insertSelect);
return GetGeneratedIdentity(obj, session, rs);
}
finally
{
session.Batcher.CloseCommand(insertSelect, rs);
}
}
else
{
// Do the insert
IDbCommand statement = session.Batcher.PrepareCommand(sql.CommandType, sql.Text, sql.ParameterTypes);
try
{
// Well, it's always the first table to dehydrate, so pass 0 as the position
Dehydrate(null, fields, notNull, propertyColumnInsertable, 0, statement, session, 0);
session.Batcher.ExecuteNonQuery(statement);
ProcessOutputParameters(null, fields, notNull, PropertyColumnInsertable, 0, statement, session,
0,obj); // выставляет значениея пропертям класса, которые смаплены на хранимку и помечены как output
// но не выставляет идентификатор// Fetch the generated id in a separate query. This is done inside the first try/finally block
// to keep the insert command open, so that the batcher does not close the connection.
//
// It's possible that some ADO.NET provider will not allow two open IDbCommands for the same connection,
// in that case we'll have to rewrite the code to use some sort of lock on IBatcher.if (!useOutputParameterForId)
{
SqlString idselectSql =
new SqlString(SqlIdentitySelect(IdentifierColumnNames[0], GetTableName(0)));
IDbCommand idselect =
session.Batcher.PrepareCommand(CommandType.Text, idselectSql, SqlTypeFactory.NoTypes);
IDataReader rs = null;
try
{
rs = session.Batcher.ExecuteReader(idselect);
return GetGeneratedIdentity(obj, session, rs);
}
finally
{
session.Batcher.CloseCommand(idselect, rs);
}
}
else// Получаем идентификатор
{
int paramCount = statement.Parameters.Count;
if (((IDbDataParameter) statement.Parameters[paramCount - 1]).Direction ==
ParameterDirection.Output)
{
object val = ((IDbDataParameter) statement.Parameters[paramCount - 1]).Value;
if (System.Convert.IsDBNull(val))
return null;
return val;
}
else
{
return null;
}
}
}
finally
{
session.Batcher.CloseCommand(statement, null);
}
}
}
catch (HibernateException)
{
// Do not call Convert on HibernateExceptionsthrow;
}
catch (Exception sqle)
{
throw Convert(sqle, "could not insert: " + MessageHelper.InfoString(this), sql.Text);
}
}
Плюс пришлось немного сменить след. код:
public object Insert(object[] fields, object obj, ISessionImplementor session)
{
int span = TableSpan;
object id;
if (entityMetamodel.IsDynamicInsert)
{
// For the case of dynamic-insert="true", we need to generate the INSERT SQLbool[] notNull = GetPropertiesToInsert(fields);
id = Insert(fields, notNull, GenerateInsertString(true, notNull), obj, session);
for (int j = 1; j < span; j++)
{
Insert(id, fields, notNull, j, GenerateInsertString(notNull, j), obj, session);
}
}
else
{
// For the case of dynamic-insert="false", use the static SQLif (IsIdentifierAssignedByInsert)
id = Insert(fields, PropertyInsertability, SqlIdentityInsertString, obj, session); //вызов вышеприведенной ф-цииelse///if assigned by stored proc through out parameter
{
//SqlCommandInfo stubCmd = new SqlCommandInfo("select null");
id = Insert(fields, PropertyInsertability, SqlInsertStrings[0], obj, session); //вызов вышеприведенной ф-ции
}for (int j = 1; j < span; j++)
{
Insert(id, fields, PropertyInsertability, j, SqlInsertStrings[j], obj, session);
}
}
return id;
}
И, наконец, в самом конце, пришлось использовать свой генератор:
По сути он идентичен реализации:
public class IdentityGenerator : IIdentifierGenerator{...}
Но главное отличие — другой тип
Поэтому св-во AbstractPersister.IsIdentifierAssignedByInsert — будет всегда false (оно берется из EntityMetamodel.IdentifierProperty.IsIdentifierAssignedByInsert )
Исходя из всего вышеприведенного можно сказать следущее:
Мы смогли добавить поддержку out параметров не меняя исходников NHibernate. Мы всего-лишь через конфиги
1. Подменили драйвер
2. Использовали свой персистер и генератор
Здравствуйте, Ziaw, Вы писали:
Z>Здравствуйте, D.Triton, Вы писали:
Z>Вижу уже не первый вопрос по маппингу с хранимками. Z>Никак не пойму зачем использовать хранимые процедуры если используется NHibernate и затем мужественно бороться с побочными эффектами возникающими при этом.
Иногда такое бывает, что заказчик "упирается рогом" и требует
И на все твои доводы просто закрывает глаза.
В некоторых случаях, когда заказчик "технически грамотный" приходится бороться с ветряными мельницами
Здравствуйте, снежок, Вы писали:
С>Философская и флеймогонная тема.
Флейма по поводу использования ХП достаточно, меня интересует конретно связка SP+NH.
С>Могу лишь высказать свои суждения вдополнение к выше приведенным по поводу использования хп, которые вполне могут быть оспорены (только не будем это делать здесь, ладно? . С>1. По моему мнению с БД нужно работать аккуратно, потому выделяю CRUD в виде хп.
Вы пропустили логическую цепочку, а без нее это напоминает утверждение: на автомобиле надо ездить аккуратно, поэтому я всегда одеваю перчатки. Оба утверждения более менее понятны, непонятно только как из первого следует второе.
С>2. хп и вью дают некоторый уровень абстракции работы с БД.
Уровня абстракции который дает NH недостаточно?
С>3. Маппинг непосредственно на таблицы "связывает" руки разработчику БД. Т.е. не получается абстрагироваться от приложения, не получается жестко разделить роли. Разработчику/администратору БД приходится более тесно общаться с другими членами комманды, он теперь обязан знать маппинг NH и оценивать свои процедуры рефакторинга с оглядкой на маппинг.
Какой рефакторинг можно произвести не оглядываясь на маппинг хранимых процедур и нельзя произвести имея маппинг на таблицыи view?
С>4. DBA раздражает каша, которую он видит в профайлере. Ему трудно анализировать возникающие проблемы.
Тут вообще непонятно, каша там или нет зависит только от запросов, при чем тут хранимки если в них будут те же самые запросы?
Архитектура приложения выбирается исходя из личных эстетических соображений DBA? А придет другой DBA, с другими мерками?
С>5. Часто существуют жесткие требования по безопасности, когда закрывают доступ к таблицам и дают доступ только к хп и вью.
Эта причина выглядит достаточно логичной. Хотелось бы пример, в котором безопасность обеспечивается ХП.
С>6. Нет хп — нет возможности по-нормальному(самостоятельно) провести тестирование создания объектов БД, например, с помощью DbUnit, т.е. опять же без оглядки на код среднего звена/клиента.
Тестирование NH? Насколько правильно он генерирует insert/update/delete? Уверены, что это необходимо?
Вы берете ORM продукт, отрезаете от него две основные функции: генерацию запросов и CRUD операций, добавляете неявный слой логики приложения, получаете кучу проблем решаемых зачастую героическими усилиями
. Что остается? Маппинг результатов? Unit of work? Кеш который рискует выдать неверные данные, т.к. расчитан на то, что в БД лежит именно то, что сохранили, а счастливый DBA исправил ХП не вдаваясь в тонкости работы NH? Необходимость хардкодить все CRUD операции и поддерживать эту кучу кода?
Здравствуйте, D.Triton, Вы писали:
DT>Иногда такое бывает, что заказчик "упирается рогом" и требует
DT>И на все твои доводы просто закрывает глаза.
DT>В некоторых случаях, когда заказчик "технически грамотный" приходится бороться с ветряными мельницами
Кстати, а вы не рассматривали возможность вызова хранимок в instead of триггерах? Есть вероятность обойтись меньшей кровью.