Работа с БД со множеством связей (Access 2000 + ADO.NET)

_ArtemK_
Дата: 06.09.2005 14:10:57
Расскажу все сначала. Имеется БД в Access
(
24 таблицы,
все теблицы имеют ключ - поле с именем ID (Autoincrement),
все таблицы связаны друг с другом (каскадное удаление/обновление)
)

В приложении (WinForms) я хочу загузить всю БД из файла в DataSet поработать с DataSet'ом и сохранить изменения обратно в файл (структура DataSet'а полностью такая же как и у оригинальной БД - со всеми связями и ключами).

БД из файла в ДатаСет гружу следующим образом:
открываю соединение,
каждая таблица загружается отдельно своим ДатаАдаптером,
для каждого ДатаАдаптера создается КоммандБилдер,
подписываюсь на RowUpdated Адаптера, чтобы корректировать поле ID, когда таблица будет обновляться обратно в БД
ДатаАдаптер сохраняется в хеш-массиве (чтобы использовать его же затем для вызова метода Update() для данной теблицы)
закрываю соединение.

ДатаСет в БД пытаюсь сохранить следующим образом:
открываю соединение,
Для всех таблиц:
{
получаю изменения, внесенные в Датасет
отыскиваю в хеш-массиве по имени таблицы нужный ДатаАдаптер,
вызываю Update() найденного Адаптера,
}
закрываю соединение.

Проблема: например, при добавлении трех записей в еще пустую таблицу ДатаСета он генерирует для записей следующие ID:
0
1
2

Пытаюсь передать изменения в Аксесс. Передается первая строка, но Аксесс сам для нее сгенерирует ID, равное 1 (а не 0, как до этого сгенерировал ДатаСет). Пытаюсь вернуть ID, сгенерированное Аксессом в Датасет (в обработчике события RowUpdated() запрашивается @@IDENTITY):

private static void OnRowUpdated(object sender, OleDbRowUpdatedEventArgs args)
		{
			// Include a variable and a command to retrieve the identity value from the Access database.
			int newID = 0;
			OleDbCommand idCMD = new OleDbCommand("SELECT @@IDENTITY", odcConn);

			if (args.StatementType == StatementType.Insert)
			{
				// Retrieve the identity value and store it in the ID column.
				newID = (int) idCMD.ExecuteScalar();
				args.Row["ID"] = newID;
			}
		}
но когда программа доходит до строки
args.Row["ID"] = newID;
генерируется исключение, т.к. в ДатаСете уже существует поле ID со значением 1

Собственно вопрос: как с этим бороться?????

Сам подход для работы с БД верно выбран??? (то, что я загружаю в ДатаСет ВСЮ БД??? то, что БД загружается отдельно по табличкам???)

Класс для работы с Датасетом приведен ниже (здесь грузятся не все таблицы из БД, а только четыре).
Для доступа к ДатаСету нужно из программы обратится к полю CDBMan.Instance.dstbData - если ДатаСет уже заполнен, то получаем ссылку на него, если еще не заполнен, то создается новый и заполняется.
Для записи изменений обратно в БД - метод CDBMan.vUpdate()


using System;
using System.Data;
using System.Data.OleDb;
using System.Collections;
using System.Windows.Forms;


namespace TestBed2006
{
	/// <summary>
	/// Summary description for CDBMan.
	/// </summary>
	public class CDBMan
	{
		private static object lockFlag = new object();
		private static CDBMan instance;
		private static string sconnectionstring = 
"Jet OLEDB:Global Partial Bulk Ops=2;" +
"Jet OLEDB:Registry Path=;" +
"Jet OLEDB:Database Locking Mode=1;" +
"Data Source=\"D:\\ArtemK\\Projects\\TestBed\\Data\\TestBed2006.mdb\";" + "Mode=Share Deny None;" +
"Jet OLEDB:Engine Type=5;" +
"Provider=\"Microsoft.Jet.OLEDB.4.0\";" +
"Jet OLEDB:System database=;" +
"Jet OLEDB:SFP=False;" +
"persist security info=False;" +
"Extended Properties=;" +
"Jet OLEDB:Compact Without Replica Repair=False;" +
"Jet OLEDB:Encrypt Database=False;" +
"Jet OLEDB:Create System Database=False;" +
"Jet OLEDB:Don't Copy Locale on Compact=False;" +
"User ID=Admin;Jet OLEDB:Global Bulk Transactions=1";

		private static OleDbConnection odcConn = new OleDbConnection(sConString);
		private static Hashtable htAdapters = new Hashtable(); 

		public dsTestBed2006 dstbData = new dsTestBed2006();

		public static string sConString
		{
			get
			{
				return sconnectionstring;
			}
			set
			{
				sconnectionstring = value;
				vReload();
			}
		}
		
		public static void vReload()
		{
			instance = null;
		}

		private static void vFillTable(string sTblName)
		{
			OleDbDataAdapter oddaAdapt = new OleDbDataAdapter();		

			oddaAdapt.SelectCommand = new OleDbCommand("select * from " + sTblName, odcConn);
			OleDbCommandBuilder odcbBuilder = new OleDbCommandBuilder(oddaAdapt);
			oddaAdapt.Fill(instance.dstbData.Tables[sTblName]);

			oddaAdapt.RowUpdated += new OleDbRowUpdatedEventHandler(OnRowUpdated);

			htAdapters.Add(sTblName, oddaAdapt);
		}

		private static void vUpdateTable(string sTblName)
		{
			DataSet dsTmp = instance.dstbData.GetChanges();

			if (dsTmp != null)
			{
				OleDbDataAdapter oddaAdapt = (OleDbDataAdapter) htAdapters[sTblName];
				oddaAdapt.Update(dsTmp.Tables[sTblName]);
			}
		}

		private static void OnRowUpdated(object sender, OleDbRowUpdatedEventArgs args)
		{
			// Include a variable and a command to retrieve the identity value from the Access database.
			int newID = 0;
			OleDbCommand idCMD = new OleDbCommand("SELECT @@IDENTITY", odcConn);

			if (args.StatementType == StatementType.Insert)
			{
				// Retrieve the identity value and store it in the ID column.
				newID = (int) idCMD.ExecuteScalar();
				args.Row["ID"] = newID;
			}
		}


		public static void vUpdate()
		{
			lock(lockFlag) 
			{
				if(instance != null)
				{
					odcConn.Open();

					vUpdateTable("TesterCategory");
					vUpdateTable("TesterExemplar");
					vUpdateTable("SysPower");
					vUpdateTable("SysAnlComm");

					odcConn.Close();
				}
			}
		}

		public static CDBMan Instance
		{
			get
			{
				lock(lockFlag) 
				{
					if(instance == null)
					{
						instance = new CDBMan();
						odcConn = new OleDbConnection(sConString);

						odcConn.Open();

						vFillTable("TesterCategory");
						vFillTable("TesterExemplar");
						vFillTable("SysPower");
						vFillTable("SysAnlComm");

						odcConn.Close();
					}
				}
				return instance;
			}
		}
	}
}

Артем1
Дата: 06.09.2005 14:45:05
Весь пост не осилил. :)
Возможно вам поможет такое решение:
В таблицах датасета настраиваете автоинкремент с 0-я и с пририщением в -1. На связях между таблицами датасета настраиваете каскадные обновления.
После сохранения родительской таблицы в БД оттуда считывается сохраненная строка целиком (включая новый, сгенерированный акцессом, первичный ключ). По каскаду новый валидный первичный ключ обновляет подчиненные записи в других таблицах. Потом можно и их сохранять в бд. Вот как то так.

зы: прием не мой, вычитал в книжке сеппы. наверняка можно найти в инете более подробное описание.

ззы: если не правильно понял вопрос, то сорри :)
Alex_2003
Дата: 06.09.2005 15:03:00
А почему используете CommandBuilder? Не хочется самому писать адаптеры?
Лучше бы написать свой адаптер для вставки и обновления. Если у вас поле ID в Access типа(Identity), то не нужно будет передавать значение этого поля в БД из DataSet при добавлении новой записи. Тогда проблемы не будет как таковой.
_ArtemK_
Дата: 06.09.2005 15:16:17
Артем1

Возможно вам поможет такое решение:
В таблицах датасета настраиваете автоинкремент с 0-я и с пририщением в -1. На связях между таблицами датасета настраиваете каскадные обновления.
После сохранения родительской таблицы в БД оттуда считывается сохраненная строка целиком (включая новый, сгенерированный акцессом, первичный ключ). По каскаду новый валидный первичный ключ обновляет подчиненные записи в других таблицах. Потом можно и их сохранять в бд. Вот как то так.


Действительно так дожно работать. Красиво придумано. Спасибо за ответ.
_ArtemK_
Дата: 06.09.2005 15:24:22
Alex_2003
А почему используете CommandBuilder? Не хочется самому писать адаптеры?
Лучше бы написать свой адаптер для вставки и обновления. Если у вас поле ID в Access типа(Identity), то не нужно будет передавать значение этого поля в БД из DataSet при добавлении новой записи. Тогда проблемы не будет как таковой.


К сожалению в моем случае так проблему решить не получиться. Если я оставляю назначение ID на усмотрение Аксесса и не буду возвращать его обратно в ДатаСет, то при обновлении вначале родительской таблицы, а затем дочерней, у дочерней foreign key'и все поедут (ведь primary key'и в родительской таблице Аксесс поизменял, а в дочерних остались старые значения). Если бы связей не было, то таких проблем и не возникло бы. Все же спасибо за ответ.
Alex_2003
Дата: 06.09.2005 16:49:17
Да,с Access я давно не работал. У меня БД SQL Server, там решение проще получается, т.к. есть замечательная функция SCOPE_IDENTITY(), которая возвращает новый ID в ХП, функции или пакете.
Но в Access тоже можно сделать след. образом:
Для адаптера (InsertCommand) пишете что-то вроде:
"INSRET INTO tablename(...) values(...);
SELECT * FROM tablename WHERE ID = (SELECT max(ID) FROM tablename)
"
Тогда после добавления ACCESS Вам вернет новый ID. И в Датасет Вы получите то что нужно.
Mik Prokoshin
Дата: 08.09.2005 11:28:56
А Вы Relation'ы настройте и будет Вам счастье с автоматическим обновлением foreign ключей. ADO.NET в отличие от старого АДО с идентити полями отлично умеет работать !
_ArtemK_
Дата: 08.09.2005 12:00:37
Проблему решил следующим образом: настроил в DataSet'е для полей ID отрицательное автоприращение. Dataset генерирует для новых строк отрицательные ID. Теперь при передаче добавленных строк обратно в Access, он сам генерирует ID для новых строк (положительное значение). Спрашиваю у Access'а новый ID и отдаю его обратно Dataset'у, тот уже обновляет у себя все внешние ключи в дочерних таблицах (по Relations).