سعید غفاری
سعید غفاری
خواندن ۷ دقیقه·۵ ماه پیش

پشتیبان گیری از دیتابیس محلی (Android/.NET)


سلام.
یکی از روش های کارآمد برای پشتیبان گیری از دیتابیس محلی آپلود کردن یک فایل CSV روی سروره این روش مزایا و معایب خاص خودشو داره که بررسی میکنیم بعد میریم سراغ پیاده سازی .

مزایا

  • سادگی و خوانایی: فایلهای CSV ساده هستند و به راحتی میشه اونا رو تو هر ویرایشگر متنی باز کرد. ساختار متن ساده این فایلها باعث میشه که همه بتونن به راحتی محتواشو بفهمن.
  • قابلیت انتقال: فایلهای CSV میتونن به راحتی بین سیستمهای مختلف منتقل شن. اکثر نرمافزارهای دیتابیس و ابزارهای تحلیل داده میتونن فایلهای CSV رو وارد یا صادر کنن.
  • حجم کم: در مقایسه با فرمتهای دیگه، فایل های CSV معمولاً حجم کمتری دارن چون شامل اطلاعات اضافی مثل متادیتا نیستن.
  • سازگاری گسترده: تقریباً همه زبانهای برنامهنویسی و سیستمهای دیتابیس میتونن با فایلهای CSV کار کنن.

معایب

  • عدم پشتیبانی از ساختارهای پیچیده: فایلهای CSV فقط دادههای جدولی رو ذخیره میکنن و نمیتونن ساختارهای پیچیده مثل انواع دادههای تودرتو یا سلسله مراتبی رو مدیریت کنن.
  • عدم پشتیبانی از انواع دادهها: تو فایلهای CSV همه دادهها به صورت رشته ذخیره میشن و نوع واقعی دادهها (مثل تاریخ، عدد صحیح، عدد اعشاری) از بین میره. این مسئله میتونه مشکل ساز بشه.
  • نبود امنیت: فایلهای CSV به طور پیشفرض مکانیزم امنیتی و رمز نگاری ندارن. بنابراین اطلاعات حساس تو این روش ممکنه در معرض خطر باشه.
  • نبود استاندارد واحد: هرچند فایلهای CSV استاندارد مشخصی دارن، ولی در عمل ممکنه تفاوتایی تو نحوهی پشتیبانی و تفسیر این فرمت بین ابزارها و سیستمهای مختلف وجود داشته باشه.که می تونه مشکل ساز بشه.
  • پشتیبانی ضعیف از دادههای بزرگ: اگه دیتابیس شما حجم زیادی داشته باشه ، استفاده از فایلهای CSV ممکنه مناسب نباشه و زمان زیادی برای پشتیبانگیری و بازیابی دادهها لازم باشه.

با توجه به مزایا و معایبی که گفتم بستگی به نوع پروژه شما و تصمیم خودتون داره که از این روش استفاده کنین یا نه . به هر حال بریم سراغ پیاده سازی .

پیاده سازی سمت اندروید

پیش فرض ما تو این سولوشن استفاده از room و retrofit هست.

قبل از همه چیز این خط رو به فایل build.gradle پروژه اضافه کنید و پروژه رو sync کنید

implementation (&quotcom.opencsv:opencsv:5.5.2&quot)

برای اینکه قابل فهم تر باشه از بیرونی ترین فانکشن شروع کنیم و پیش بریم.

1- یه فانکشن بکاپ میسازیم مثل کد زیر:

fun backupDb() { try { val dataBasePart = getDatabaseAsPart() val result = retrofitService.uploadDatabase( dataBasePart ) if (result.isSuccessful) { //پیاده سازی لازم } else { //پیاده سازی لازم } } catch (e: Exception) { e.printStackTrace() } } }

تو این فانکشن دوتا فانکشن دیگه فراخوانی شده یکی کال رتروفیت سرویسمونه یکی دیگه هم فانکشن
getDatabaseAsPart . اول چند تا نکته رو بگم بعد بریم سراغ این دوتا فانکشن .

نکته اول: retrofitService رو باید تو کلاسی که این فانکشن هست اینیشیالایز کنید یا اینجکت کنید که اینجا من نیاوردم.
نکته دوم: باید فایلی که قراره آپلودش کنیم در قالب یک Part به رتروفیت کال داده بشه که فانکشن getDatabaseAsPart این کارو انجام خواهد داد.
نکته سوم: بهتره و یا لازمه که تو آپلود از متدای امنیتی و Authentication و همچینین rate Limiter استفاده کنید که باز برای اینکه قیمه ها رو نریزیم تو ماستا من اینجا نیاوردم.

2- فانکشن رتروفیت سرویس:

@Multipart @POST(&quotbackup/uploadDb&quot) suspend fun uploadDatabase( @Part dataBase: MultipartBody.Part ): Response<String>

3- فانکشن getDatabaseAsPart:

fun getDatabaseAsPart( ): MultipartBody.Part { val dbFile: File = csvExportUtil.exportDatabaseToCsv() //این کلاس در ادامه ساخته خواهد شد val requestFile = RequestBody.create(&quotapplication/octet-stream&quot.toMediaTypeOrNull(), dbFile) return MultipartBody.Part.createFormData(&quotDatabaseFile&quot, dbFile.name, requestFile) }

خب بزارید این فانکشنو یه ذره دقیق تر بررسی کنیم . تو مرحله اول یه مقدار داریم از نوع File که فانکشن exportDatabaseToCsv اون رو به ما حواهد داد که یه فایل csvهست. بعد یه RequestBody از فایلمون میسازیم توسط فانکشن create که دو تا ورودی میگیره و مرحله آخر پارتی که میخواستیمو بر میگردونه.باز یادتون باشه کلاس csvExportUtil رو که خواهیم ساخت در کلاس این فانکشن اینیشیالایز یا اینجکت کنید

بیشتر بدانید!!:
"application/octet-stream"" این MIME type نشاندهنده این است که دادهها به عنوان یک جریان باینری (Binary Stream) هستند و نوع خاصی از دادهها مشخص نشده است. ()toMediaTypeOrNull این تابع رشته را به یک شیء MediaType تبدیل میکند. اگر تبدیل موفق نباشد، null برمیگرداند.

خب بریم سراغ فانکشن exportDatabaseToCsv

3- کلاس csvExportUtil و فانکشن exportDatabaseToCsv:

class CsvExportUtil() { fun exportDatabaseToCsv(context:Context):File { val csvFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), &quotname.csv&quot) var csvWriter: CSVWriter? = null try { csvWriter = CSVWriter(FileWriter(csvFile)) var cursor: Cursor? =yourDao.getAllUsersCursor() // فرض کنید این متد تمام ردیفهای جدول را باز میگرداند // نوشتن سطر عنوانها csvWriter.writeNext(cursor!!.columnNames) // نوشتن دادههای جدول while (cursor.moveToNext()) { val row = Array(cursor.columnCount) { i -> cursor.getString(i) } csvWriter.writeNext(row) } cursor.close() } catch (e: Exception) { e.printStackTrace() } finally { try { csvWriter?.close() } catch (e: IOException) { e.printStackTrace() } } return csvFile } }

خب توضیحات این فانکشن اینطوریه که:
یه context میگیره که برا کلاس File لازمش داریم .
csvFile یه فایل تو پوشه داکیومنت میسازه با پسوند csv و اسم name .
یه مقدار میسازیم از نوع CSVWriter که فایل رو تو تابع FileWriter از ما به عنوان ورودی می گیره.
بعد یه cursor میسازیم که اطلاعاتو از جدول مورد نظرمون میخونه که فانکشنشو تو ادامه مینویسم براتون .
حالا به تابع csvWriter دستور میدیم که اسم ستون های جدولمون رو ت سطر اول بنویسه
بعدش هم با یه loop خط به خط اطلاعات جدولمون رو تو فایل csv می نویسیم . ( برا اینکه مسئله روشنتر شه یه نمونه فایل csv رو ببینید) .
در آخر هم cursor و writer رو بستیم که دچار memory leak نشیم.

بیشتر بدانید:

نمونه فایل csv
نمونه فایل csv


4- فانکشن getAllUsersCursor :

@Query(&quotSELECT * FROM users&quot) fun getAllUsersCursor(): Cursor

تو این فانکشن همه یوزر هامونو در قالب یک Cursor از تیبل میکشیم بیرون و تقریبا کارمون با اندروید تمومه.


پیاده سازی سمت Dot Net

خب از اونجاییکه من android developer هستم فقط فانکشن های سمت بک اند رو بدون توضیح کلی در اختیارتون میزارم و اگر اشتباهی تو این موارد هست از شما و متخصصینش عذر خواهی میکنم.

1-کنترلر backup endpoint

public static class BackUpEndPoints { public static RouteGroupBuilder MapBackupEndPoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup(&quot/backup&quot).WithParameterValidation(); group.MapPost(&quot/uploadDb&quot,async ( IFileService iFileService, [FromForm] GetDatabaseDto dbDto )=>{ if (dbDto.DatabaseFile != null) { var fileResult = iFileService.SaveDatabase(dbDto.DatabaseFile,&quotfolder name&quot ); if (fileResult.Item1 == 1) { return Results.Ok(&quotبا موفقیت آپلود شد&quot); } else { return Results.Conflict(new { error = fileResult.Item2}); } } else { return Results.Conflict(new { error = &quotفایل خالی است&quot}); } }); return group; } }

2- اینترفیس IFileService

public interface IFileService { public Tuple<int, string> SaveDatabase(IFormFile sqlightDb,String userphone); //تابع پایینی برای دیافت بکاپ از سرور public Tuple<int, string> GetDatabasePath(String userphone); }

3- پیاده سازی اینترفیس IFileService

public class FileService:IFileService { private IWebHostEnvironment _environment; public FileService(IWebHostEnvironment environment) { _environment = environment; } private readonly string _backupPath = &quot/app/files/backups&quot public Tuple<int, string> SaveDatabase(IFormFile sqlightDb, string folderName) { try { var contentPath = this._environment.ContentRootPath; var path = Path.Combine(_backupPath, folderName); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var ext = Path.GetExtension(sqlightDb.FileName); var allowedExtensions = new string[] { &quot.csv&quot }; if (!allowedExtensions.Contains(ext)) { string msg = string.Format(&quotOnly {0} extensions are allowed&quot, string.Join(&quot,&quot, allowedExtensions)); return new Tuple<int, string>(0, msg); } var newFileName = &quotusers&quot var fileWithPath = Path.Combine(path, newFileName); if (System.IO.File.Exists(fileWithPath)) { System.IO.File.Delete(fileWithPath); } using (var stream = new FileStream(fileWithPath, FileMode.Create)) { sqlightDb.CopyTo(stream); } return new Tuple<int, string>(1, &quotbackups/&quot + folderName+ &quot/&quot + newFileName); } catch (Exception ex) { return new Tuple<int, string>(0, &quotError has occured&quot); } } }

در آخر این چند خط رو به program .cs اضافه کنید:

//var builder = WebApplication.CreateBuilder(args); builder.Services.AddScoped<IFileService, FileService>(); //var app = builder.Build(); app.MapBackupEndPoints();

مجددا عرض کنم خدمتتون من متخصص دات نت نیستم و این چند خط رو ارائه کردم برا شما عزیزان اندروید دولوپری که خودتون بک اند رو میزنین . فقط خواستم یه ذهنیتی براتون ایجاد بشه نه اینکه دقیقا این کد رو پیاده سازی کنین.

تو پست بعدی توضیحات دریافت فایل پشتیبان رو منتشر می کنم . امیدوارم که تا اینجاش قابل فهم بوده باشه و اگر اشکالی هست ممنون میشم تو نظرات بهم بگین . فعلا خدا حافظ...

sqliteandroidbackup
شاید از این پست‌ها خوشتان بیاید