الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

درس: كيفية تسجيل ملفات في قاعدة بيانات Sql Server

بدأه Mohammad Elsheimy في 19 يوليو 2009 · 16 رد · 4,863 مشاهدة · في Microsoft Visual C#.NET
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

قمت بكتابة هذا الدرس سابقا في موقعي
.

نظرة خاطفة

درسنا اليوم يتكلم عن طريقة تسجيل البيانات Binary Data في قاعدة بيانات SQL Server. فمثلا بدلا من تسجيل النصوص والأرقام فقط، درسنا اليوم يتكلم عن طريقة أخرى لتسجيل البيانات وهي تسجيل ملفات أو بالأصح بيانات ثنائية Binary Data مثل ملفات الصور والفيديو والـ Word وملفات الكتابة وجميع الملفات. فجميع الملفات كما نعرف هي ملفات ثنائية Binary. يشرح الدرس أولا مقدمة عن هذه الملفات وكيفية التعامل معها في SQL Server ثم ندخل في طريقة تسجيل هذه البيانات واسترجاعها.

مع الدرس مثال يشرح كيفية تسجيل الملفات واسترجاعها من قاعدة بيانات SQL Server.

مقدمة

جميع الملفات على الجهاز هي كما نعرف ملفات ثنائية أو Binary، فكما تعرف أن جميع البيانات يتم تمثيلها داخل الجهاز بنظام ثنائي فمثلا الرقم 5 يتم تمثيلها هكذا 101 والحرف a له الكود 65 فلهذا يتم تمثيله هكذا 1000001.

يمكنك استخدام الآلة الحاسبة Calculator في التحويلات. فقط قم بالتغيير إلى الوضع "علمي Scientific".

وهذه البيانات يطلق عليها لفظ BLOBs أي Binary Large Objects أي العناصر الثنائية الكبيرة. ونرمز بمصطلح BLOB إلى جميع البينات من ملفات كتابة وصور وفيديو وألعاب وبرامج وغيرها فهي جميعها ملفات (أو عناصر) ثنائية كبيرة الحجم.

لماذا يختلف التعامل مع بيانات BLOB عن غيرها من البيانات الأخرى في SQL Server؟

بالطبع البيانات BLOB يتم تسجيلها بطريقة مختلفة عن طريقة تسجيل البينات من الأنواع الأخرى مثل النصوص والأرقام وذلك بسبب شيئين:

  1. في الغالب أنت لا تعرف حجم البيانات التي يتم تسجيلها. بعكس مثلا إذا كنت تحاول تسجيل نص مثلا اسم شخص في قاعدة البيانات فأنت تطلب من المستخدم إدخال اسمه والذي يجب أن لا يزيد عن 30 حرف مثلا.
  2. السبب الثاني وهو أن البيانات هذه تكون أحيانا حجمها كبير جدا فمثلا تخيل أنك تحاول أن تقوم بتسجيل ملف فيديو حجمه 50 ميجا بايت! فماذا الحل؟ بالطبع أولا، سوف يزيد حجم قاعدة البيانات بحجم هذا الملف المراد تسجيله. وثانيا عند التعامل معه بالطبع أنت لا يمكنك التعامل مع 50 ميجا بايت مرة واحدة فهذا سوف يسبب بطء وفي الغالب توقف برنامجك، ولهذا سوف تحتاج إلى تسجيله واسترجاعه دفعة Chunk بدفعة أخرى. فمثلا بدلا من تسجيل 50 ميجا بايت مرة واحدة يمكنك عمل دوارة Loop تقوم بتسجيل 100 ك.ب. في المرة مثلا. (وهذا أيضا يعتبر إلى حد ما رقم كبير جدا).

للأسف Access لا يمكنه تسجيل بيانات من هذا النوع.

في شرحنا في هذا الدرس سوف نشرح على قاعدة بيانات SQL Server بسيطة جدا يمكنك إنشائها بنفسك وتسمى FileStore. هذه القاعدة لا تحتوي إلا على جدول واحد اسمه MyFiles وهذا الجدول يحتوي على فقط عمودين الأول اسمه Filename ويحتوي على اسم الملف وهو من نوع نصي أي nvarchar بالحجم 260 وبالطبع هو المفتاح الأساسي Primary Key (PK) لهذا الجدول. أما العمود الآخر فهو يسمى Data وهو من نوع image ويستخدم هذا العمود لتسجيل بيانات الملف. لاحظ أنه النوع image للعمود يستخدم لتسجيل البيانات binary أي بيانات BLOB.

تسجيل بيانات BLOB

الكود التالي هو كود بسيط جدا عبارة عن دالة لها مدخل Argument واحد وهو عبارة عن اسم الملف المراد تسجيله. تقوم هذه الدالة بقراءة هذا الملف وتسجيله في قاعدة البيانات. لاحظ الكود.

بالطبع لا تنسى إضافة Reference للمكتبة System.Data.dll وبالطبع يتبعها System.Xml.dll. ولا تنسى أيضا إضافة جملتي using (أو Imports في VB.NET) للـ Namespaces المطلوبة وهي System.Data.SqlClient و System.IO.

// C# Code
   static void StoreFile(string filename)
   {
	   SqlConnection connection = new SqlConnection
		   ("Server=(local); Initial Catalog = FileStore; Integrated Security = SSPI");

	   SqlCommand command = new SqlCommand
		   ("INSERT INTO MyFiles VALUES (@Filename, @Data)", connection);

	   command.Parameters.AddWithValue("@Filename", Path.GetFileName(filename));
	   command.Parameters.AddWithValue("@Data", File.ReadAllBytes(filename));

	   connection.Open();

	   command.ExecuteNonQuery();

	   connection.Close();
   }

   -------------------------------------------

   ' VB.NET Code
   Sub StoreFile(ByVal filename As String)
	   Dim connection As New SqlConnection( _
		   "Server=(local); Initial Catalog = FileStore; Integrated Security = SSPI")

	   Dim command As New SqlCommand( _
		   "INSERT INTO MyFiles VALUES (@Filename, @Data)", connection)

	   command.Parameters.AddWithValue("@Filename", Path.GetFileName(filename))
	   command.Parameters.AddWithValue("@Data", File.ReadAllBytes(filename))

	   connection.Open()

	   command.ExecuteNonQuery()

	   connection.Close()
   End Sub

شرح الكود

قمنا أولا بإنشاء Connection لقاعدة البيانات ثم قمنا بعد ذلك بإنشاء العنصر SqlCommand والذي سيحوي أمر SQL المراد تنفيذه لإضافة البيانات.

بالطبع، ولأن هذا هو التطبيق الأفضل، قمنا بإستخدام مدخلات Parameters لأمر SQL بدلا من كتابتها في الأمر نفسه وذلك أأمن وأفضل وأسرع ولهذا استخدمنا عناصر من نوع SqlParameter.

أخيرا وبعد فتح الاتصال مع القاعدة وتنفيذ الأوامر قمنا بإغلاق الاتصال مع القاعدة.

يفضل إغلاق جميع الاتصالات Connections وجميع الأوامر Commands التي تم إنشائها وبالطبع جميع العناصر التي تتعامل مع قواعد البيانات وتوفر لنا أمر للإغلاق مثل أمر Close() أو Dispose() وذلك توفيرا لموراد الجهاز والحفاظ على أداء برنامجك.

استرجاع بيانات BLOB

الكود التالي يوضح كيفية استرجاع البينات التي قمنا بإدخالها مسبقا. لاحظ أن الكود يأخذ اسم الملف كمدخلات ويقوم بإرجاع بيانات هذا الملف في هيئة مصفوفة Array من نوع Byte.

// C# Code
   static byte[] RetrieveFile(string filename)
   {
	   SqlConnection connection = new SqlConnection(
		  "Server=(local); Initial Catalog = FileStore; Integrated Security = SSPI");

	   SqlCommand command = new SqlCommand(
		  "SELECT * FROM MyFiles WHERE Filename=@Filename", connection);

	   command.Parameters.AddWithValue("@Filename", filename);

	   connection.Open();

	   SqlDataReader reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess);

	   reader.Read();

	   MemoryStream memory = new MemoryStream();

	   long startIndex = 0;
	   const int ChunkSize = 256;
	   while (true)
	   {
		   byte[] buffer = new byte[ChunkSize];

		   long retrievedBytes = reader.GetBytes(1, startIndex, buffer, 0, ChunkSize);

		   memory.Write(buffer, 0, (int)retrievedBytes);

		   startIndex += retrievedBytes;

		   if (retrievedBytes != ChunkSize)
			   break;
	   }

	   connection.Close();

	   byte[] data = memory.ToArray();

	   memory.Dispose();

	   return data;
   }

   -------------------------------------------

   ' VB.NET Code
   Function RetrieveFile(ByVal filename As String) As Byte()
	   Dim connection As New SqlConnection( _
		   "Server=(local); Initial Catalog = FileStore; Integrated Security = SSPI")

	   Dim command As New SqlCommand( _
		   "SELECT * FROM MyFiles WHERE Filename=@Filename", connection)

	   command.Parameters.AddWithValue("@Filename", filename)

	   connection.Open()

	   Dim reader As SqlDataReader = command.ExecuteReader _
		   (System.Data.CommandBehavior.SequentialAccess)

	   reader.Read()

	   Dim memory As New MemoryStream()

	   Dim startIndex As Long = 0
	   Const ChunkSize As Integer = 256
	   While (True)
		   Dim buffer(ChunkSize) As Byte

		   Dim retrievedBytes As Long = reader.GetBytes(1, startIndex, buffer, 0, ChunkSize)

		   memory.Write(buffer, 0, CInt(retrievedBytes))

		   startIndex += retrievedBytes

		   If (retrievedBytes <> ChunkSize) Then
			   Exit While
		   End If
	   End While

	   connection.Close()

	   Dim data() As Byte = memory.ToArray()

	   memory.Dispose()

	   Return data
   End Function

شرح الكود

بعد فتح الوصلة Connection إلى قاعدة البيانات وكتابة الأمر SQL نقوم بتنفيذه عن طريق الدالة ExecuteReader وهذه الدالة تقوم بتنفيذ الأوامر وإرجاع عنصر SqlDataReader يتم عن طريقه استرجاع البيانات كما قلنا Chunk by Chunk او دفعة بدفعة. لاحظ أنه يجب تحديد الأمر CommandBehavior.SequentialAccess للدالة ExecuteReader وهذا الأمر يسمح لنا بعمل Sequential Access أو وصول تتابعي للبيانات أي أننا لا نأخذها كلها مرة واحدة بل دفعة بدفعة بشكل تتابعي.

قمنا بعد ذلك بالنداء على دالة Read الخاصة بالعنصر SqlDataReader وذلك لنجعلها تقرأ أول صف في الصفوف وهكذا كل نداء على هذه الدالة يجعلها تقرأ صف بصف وهكذا. تقوم هذه الدالة بإرجاع True إذا كان هناك صف موجود أو False إذا كان آخر صف قرأناه هو آخر صف في البينات المسترجعة.

ثم نقوم بعد ذلك باسترجاع البينات دفعة بدفعة عن طريق الدالة GetBytes الخاصة بالعنصر SqlDataReader. وهذه الدالة تأخذ خمس مدخلات Arguments وهي على الترتيب:

  1. رقم العمود. وفي هذا المثال الرقم هو 1 أي العمود الثاني.
  2. المكان الذي سوف يتم القراءة من بدايته. وهو المكان داخل البيانات Binary المخزنة وهو بالبايت. فعلى سبيل المثال إذا قمنا بكتابة المكان 1024 معنى هذا أننا سوف نقوم بالقراءة من أول الكيلو بايت الثاني وهكذا. بالطبع قمنا باستخدام العنصر startIndex لمعرفة مكان القراءة لأننا نقرأ كل مرة دفعة بدفعة.
  3. العنصر الذي سوف يتم تسجيل البينات المدخلة فيه. قمنا باستخدام مصفوفة من النوع Byte حجمها حجم الدفعة Chunk التي نقوم باسترجاعها في المرة.
  4. حجم البيانات أي الدفعة لاسترجاعها. في هذا المثال قمنا بتحديد 256 بايت كحجم للدفعة Chunk.

تقوم هذه الدالة بإرجاع عدد البايتات التي تم استرجاعها ونقوم بتسجيل هذا العدد في المتغير retrievedBytes ويساعدنا هذا العدد في معرفة إذا كنا في آخر البيانات أم لا لأن إذا كان العدد المسترجع غير العدد المطلوب (وهو 256 بايت كما حددنا) إذا فهذه آخر بيانات في الملف.

قمنا أيضا باستخدام عنصر من نوع MemoryStream لتسجيل البيانات المسترجعة فيه وهذا العنصر يقوم بتسجيل البيانات في الذاكرة وبالطبع هذا ليس حلا مثاليا لأنه ربما تكون هناك بيانات كبيرة الحجم جدا فلذلك يفضل التسجيل على ملف على الجهاز أفضل ولكن لنجعل المثال أبسط قمنا باستخدام MemoryStream.

أخيرا قمنا باسترجاع البيانات من الذاكرة عن طريق الدالة ToArray().

بالطبع جميع العناصر التي لها الدالة Close أو Dispose يجب النداء على هذه الدوال متى تنتهي من استخدامها وذلك لأن هذه العناصر تستخدم موارد كبيرة للجهاز فلذلك يجب إلغاء هذه الموارد متى يتم الانتهاء من التعامل معها. تمسى هذه العناصر Disposable Objects وهي تقوم بتطبيق الـ Interface المسماة بـ IDisposable. من هذه العناصر معظم (ربما جميع) العناصر المستخدمة مع قواعد البيانات وأيضا معظم (ربما جميع أيضا) العناصر المستخدمة مع الملفات.

المثال

مع هذا الدرس مثال يوضح كيفية تسجيل الملفات واسترجاعها من قاعدة البيانات. هذا المثال بالـ C# ويعمل على قاعدة بيانات تصميمها كالتالي:

filestore-file-table-definition.jpg

لإنشاء قاعدة البيانات إما أن تقوم بإنشائها يدويا أو باستخدام ملف الأوامر الموجود مع المثال.

خاتمة

تعلمنا في هذا المثال كيفية تسجيل ملفات في قاعدة بيانات SQL Server. كما تعرف أن حجم قاعدة البيانات سوف يزيد بحجم البيانات المسجلة فيها فلهذا يفضل أن تقوم بضغط الملفات قبل تسجيلها وأيضا يفضل ألا تقوم بتسجيل ملفات إلا إذا كانت ذا أهمية كبيرة. ويفضل أيضا وضح حد أقصى لحجم الملفات المسجلة. كما يفضل تسجيل الملفات في جدول خاص بها وألا تسجل في نفس الجدول الذي يحوي البيانات المتعلقة بها. فعلى سبيل المثال، إذا كنا نقوم ببرمجة برنامج للموارد البشرية HR لشركة يفضل ألا يتم تسجيل بيانات الشخص المتقدم للوظيفة مع السيرة الذاتية CV الخاصة به. بل يفضل وضع السير الذاتية في جدول والبيانات بجدول آخر وربط الجدولين ببعضهم وذلك حفاظا على أنه إذا كنا نريد مثلا تفحص البيانات وليست السير الذاتية لأشخاص معينين نضمن بهذا سرعة استرجاع البيانات وذلك لأن بيانات الملفات تأخذ مساحة ووقت أكبر. وهكذا...

اقرأ عن
.

تم تعديل هذه المشاركة بواسطة Mohammad Elsheimy في 19 يوليو 2009 في 20:46

#2

أخى جزاك الله كل الخير على كل ماتقدمه

لى تعليق بسيط لماذا لاتجعل دروسك كلها فى موضوع واحد متجدد حتى يسهل ذلك على الأعضاء تصفح الدروس

ووفقك الله لما يحبه ورضاه

 وممّا زادنـــي شرفــاً وتيهــاً   **  وكدت بأخمصي أطأ الثريّا

دخولي تحت قولك يا عبادي  **   وأن صيّرت أحمد لي نبيّـاً

#3

شكرا أخي على مرورك

بجد فكرة كويسة

بس مش عارف

يمكن صعب تطبيقها

ولكن أنا أقترح على الإدارة إنها تضيف قسم جوا كل منتدى يكون اسمه الدروس

بحيث إن اللي عاوز يقرأ دروس يخش في القسم ده

أما اللي عاوز يشوف الأسئلة وأجوبتها يتعامل عادي

يا ريت لو يحصل كده

شكرا ليك مرة تانية

#4
اقتباس
يمكن صعب تطبيقها

لا صعب ولا حاجه انت بس مثلا تكمل الدروس الباقيه على نفس هذا الرابط و واحده واحده يتم تثبيت الصفحه

 وممّا زادنـــي شرفــاً وتيهــاً   **  وكدت بأخمصي أطأ الثريّا

دخولي تحت قولك يا عبادي  **   وأن صيّرت أحمد لي نبيّـاً

#5

بإذن الله

فكرة كويسة

شكرا لك جزيلا

وبرضه أنا مواضيعي كلها متجمعة في موقعي "مع الدوت نت"

http://with.net.tc

إذا بتحب تشوفه وتقوللي رأيك

#6

لن اشكرك أو اقول الاف الشكر والكلمات الجميلة التى قد يكون زملائى سبقونى بها ولكن سأقول :

اللهم ارزق محمد الشيمى بكل حرف فى كل درس حسنه وضاعفها لها عشرة اضعاف ثم الى سبعمائة ضعف فلقد تركنا لك يا الله وحدك شكره لعجزنا نحن على شكره

واننى اطالب وبشدة وبكل ما املك من صوت عالى من مشرفى المنتدى الكرام تثبيت دروس اخونا محمد حتى تعم الفائدة علينا وعلى الامة كلها

والله يا محمد يا اخى بلفعل لسانى يعجز عن شكرك عن كل هذا المجهود

وفقك الله ونطمع فى المزيد اخوك تامر

#7

شكرا ليك د. تامر والله أحلى دعوة سمعتها في حياتي

وشكرا ليكم كلكم

والله كلامكم ده مشجع جدا ليا

شكرا ليكم

#9

الف الف الف الف شكر ليك يا باشمهندس بس انا كان ليا تساؤل بسيط

ال أكسس بيدعم نوع من انواع الBinary عن طريق ال OLE Object ، لو تعرف طريقة التعامل معاه برمجيا

يبقى تمام جدا

اكرر شكرى مرة أخرى

تقبل تحياتى

#10

السلام عليكم

أخواني أولا أود شكركم جدا جدا جدا

ثانيابالنسبة للأخ DotNet-XML بعد الشكر:

بإذن الله نشرح طرقOLE Objects في الدوت نت عموما قريبا بإذن الله تعالى. تقبل تحياتي.

شكرا

#11

اقتراح بسيط أخ محمد...

عند تصفحي لأسئلة و أجوبة بعض أعضاء المنتدى في مواضيع تخص SQL و Ado أراهم يستعملون string concatenation لتهيئة جمل SQL وكما تعرف أخي و كما وضحته بشكل مقتضب في هذا الدرس القيم فهذا النوع من التعامل خطير جدا وينصح الإبتعاد عن استعماله و تعويضه باستعمال SqlParameter...

فلو تكرمت أخي بدرس من دروسك القيمة في هذا الموضوع سيكون ذا فائدة كبيرة لأعضاء المنتدى.

#12

أكيد أخي طبعا كلامك مظبوط

تخيل مثلا إني كاتب كود زي ده

string conn = "SELECT [Password] From Users WHERE [Uesrname]='" + 
	this.usernameTextBox.Text + "'";

وقام جه اليوزر دخل في خانة اسم المستخدم الكلام ده مثلا

myusername';DROP TABLE 'Users

إذا كإني ببعت للقاعدة الأمر ده

SELECT [Password] From Users WHERE [Uesrname]='myusername'; 
DROP TABLE 'Users'

وبالتالي انا بنفذ دلوقتي جملتين مع بعض

الأول

جملة ال SELECT

والتانية

جملة ال DROP

واللي هتشيل الجدول بتاع المستخدمين

ملاحظة: موضوع جملتين مع بعض ده بيشتغل فقط مع SQL Server أما Access هيعمللك Error لإنه مش بيستقبل غير جملة واحدة ومش هينفذ حتى الجملة الأولى.

طبعا فكرة إننا نضيف أمر داخل جملة الـ SQL هو عبارة عن هجوم Attack على البرنامج واحد عاوز يوقعه وده الهجوم ده بنسميه SQL Injection Attack.

فبنحله بفكرة Parameterized Queries بمعنى اننا نستخدم المدخلات Parameters بدل من إننا نكتب الأمر Hard-Coded Queries زي ما شفنا وهكذا.

فباستخدام Parameters بنخلي أي حاجة يدخلها المستخدم هي النص المراد يعني كل اللي كتبه كله على بعضه مثلا هيكون اسم اليوزر ومش هيتعامل معاه على إنهم جملتين.

شكرا على الفكرة وياريت لو تشاركونا أفكاركم

تحياتي

#13

شكرا أخي على المعلومات القيمة و هذا موضوع شيق عن تقنيات SQL Injection Attacks by Exemple

تحياتي...

#14

شكرا لك أخي "محمد رضى"

#15

شكراً على الطرح الجميل


 


اتمنى ترفق مثال بالـVB.NET


#16

لديك اسلوب رائع في السرد

شكرا

#17

الاخوة الكرام

كيف استبدل عبارة dlookup بعبارة select حاولت كتابة الكود التالي

Private Sub Class_no_AfterUpdate()
Dim vname As Long
Dim crt As String
Dim db As Database
Dim rcd As Recordset

vname = Me.Class_no
Set db = CurrentDb
crt = "SELECT A_ClassName FROM Class_name WHERE room ='" & (Me!uprom) & "' and id_catacc = vname"
Set rcd = db.OpenRecordset(crt, dbOpenSnapshot)
Me.Class_name = rcd
rcd.Close
End Sub

فظهرة رسالة خطأ معلمات قليلة جدا

يرجى تبيان موضع الخطأ وشكرا

تم تعديل هذه المشاركة بواسطة sweabdm في 4 أكتوبر 2020 في 12:41

مواضيع مشابهة