![دیتابیس](https://quera.org/qbox/view/TDVsCpIzw4/databases.png)
علی بهتازگی با کتابخانهی [*GORM*](https://gorm.io/) که یک *ORM* به زبان *Go* است آشنا شده. او داکیومنت این کتابخانه را مطالعه کرده و به مرور زمان در حال پی بردن به امکانات جذاب این کتابخانه است. از آنجایی که علی فرد کنجکاوی است، او تصمیم گرفته تا سورس کد این کتابخانه را مطالعه کند و دریابد که این کتابخانه چگونه کار میکند. از آنجایی که کد این کتابخانه بسیار طولانی است و علی حوصلهی خواندن آن را ندارد. به دنبال نسخهی سادهای از یک *ORM* مشابه *GORM* میگردد تا بتواند با خواندن سورس کد آن، نحوهی کارکرد آن را متوجه شود. از شما میخواهیم تا چنین کتابخانهای را برای علی پیادهسازی کنید.
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/problemset/assignments/4367/download_problem_initial_project/157640/) دانلود کنید. ساختار فایلهای پروژه بهصورت زیر است:
```
orm
├── test
│ └── orm_sample_test.go
├── configurators.go
├── driver.go
├── go.mod
├── go.sum
├── orm.go
└── query.go
```
## استراکت `EntityConfigurator`
این استراکت در فایل `configurators.go` تعریف شده است. موجودیتهای برنامه میتوانند شامل اطلاعات مختفی نظیر نام جدول باشند. این اطلاعات میتوانند در این استراکت ذخیره شوند. متدی با نام `Table` برای این نوع داده تعریف شده است که فیلد `table` (نام جدول) مربوط به موجودیت را مقداردهی میکند. در صورت نیاز، میتوانید ساختار این استراکت را تغییر دهید، اما نباید امضای متد `Table` را تغییر دهید.
## استراکت `Driver`
قرار است *ORM* ما بتواند به دیتابیسهای مختلف متصل شود. درایورها، جزئیات پیادهسازی مربوط به *DBMS* های مختلف را مشخص میکنند. استراکت `Driver` در فایل `driver.go` تعریف شده است. همچنین یک استراکت بدون نام در متغیر `Drivers` تعریف شده که شامل دو درایور در قالب فیلدهای `PostgreSQL` و `SQLite3` است. نام درایور `PostgreSQL` را برابر با `postgres` و نام درایور `SQLite3` را برابر با `sqlite3` قرار دهید. یک فیلد با نام `PlaceHolderGenerator` نیز وجود دارد که مقدار آن یک تابع است که با دریافت تعداد پارامترهای موجود در یک کوئری *SQL* ، آرایهای از رشتهها که بیانگر *placeholder* مقادیر موجود در کوئری هستند را برمیگرداند. این تابع را برای دو درایور تعریفشده بهصورت زیر پیادهسازی کنید:
+ در درایور `PostgreSQL`، تابع `PlaceHolderGenerator` باید با دریافت `n`، مجموعهای از رشتهها به فرم `$i` (بهطوری که `i` از `1` تا `n` است) را برگرداند. برای مثال، اگر مقدار `n` برابر با `3` باشد، خروجی باید بهترتیب شامل رشتههای `$1`، `$2` و `$3` باشد.
+ در درایور `SQLite3`، تابع `PlaceHolderGenerator` باید با دریافت `n`، یک آرایه از رشتهها بهطول `n` که مقادیر آن همگی برابر با `?` هستند برگرداند.
## اینترفیس `Entity`
این اینترفیس در فایل `orm.go` تعریف شده و شامل یک متد `ConfigureEntity(e *EntityConfigurator)` است. موجودیتهای برنامه باید این اینترفیس را پیادهسازی کنند. متد `ConfigureEntity` این اینترفیس، پیکربندی اولیهی یک `Entity` را انجام میدهد (مثلاً نگهداری نام جدول مربوط به موجودیت) که به ازای هر `Entity` توسط برنامهنویس آن پیادهسازی میشود.
## استراکت `ConnectionConfig`
این استراکت شامل پوینتری به یک `sql.DB`، پوینتری به یک درایور و آرایهای از موجودیتهای برنامه است. از این `ConnectionConfig`ها در تابع `SetupConnections` استفاده میشود.
## تابع `SetupConnections`
این تابع با دریافت تعداد نامشخصی `ConnectionConfig`، اتصال مربوط به کانکشنها را برقرار میکند و مقداردهیهای اولیهی لازم برای کانکشنها را انجام میدهد.
## تابع `Find`
این تابع با دریافت کلید اصلی (عددی) یک موجودیت از نوع `T` (جنریک)، اطلاعات مربوط به سطر موردنظر از دیتابیس را باید در قالب یک آبجکت از نوع `T` برگرداند. نام ستونها در جدول بهصورت `snake_case` هستند، اما نام فیلدهای نوع دادهی `T` بهصورت `PascalCase` است. برای مثال، اگر در یک موجودیت، فیلدی با نام `BodyText` داشته باشیم، در جدول متناظر با موجودیت، ستونی با نام `body_text` وجود خواهد داشت. تضمین میشود که فیلدها برای ستونهای مختلف جدول تعریف شدهاند.
## تابع `All`
این تابع، اطلاعات مربوط به سطرهای جدول موردنظر از دیتابیس (با توجه به نوع دادهی جنریک) را باید در قالب آرایهای از `T`ها برگرداند.
## تابع `First`
این تابع باید اطلاعات اولین سطر (بر اساس کلید اصلی جدول) در جدول موردنظر (با توجه به نوع دادهی جنریک) را در قالب آبجکتی از نوع `T` برگرداند.
## تابع `Last`
این تابع باید اطلاعات آخرین سطر (بر اساس کلید اصلی جدول) در جدول موردنظر (با توجه به نوع دادهی جنریک) را در قالب آبجکتی از نوع `T` برگرداند.
## استراکت `QueryBuilder`
این استراکت شامل اطلاعات یک کوئری برای تبدیل به فرمت *SQL* است که در فایل `query.go` تعریف شده. آن را به دلخواه پیادهسازی کنید.
تابع `NewQueryBuilder` را بهگونهای پیادهسازی کنید که یک نمونهی جدید از نوع `QueryBuilder` برگرداند.
متدهای زیر برای استراکت `QueryBuilder` تعریف شده که باید آنها را پیادهسازی کنید:
+ متد `Get`: این متد باید کوئری مدنظر (با توجه به متدهای فراخوانیشده روی `QueryBuilder`) را اجرا کرده و نتیجه را در قالب یک آبجکت از نوع `OUTPUT` (بهصورت جنریک) برگرداند. در اینصورت، تنها اولین نتیجه برمیگردد.
+ متد `All`: این متد باید کوئری مدنظر (با توجه به متدهای فراخوانیشده روی `QueryBuilder`) را اجرا کرده و همهی نتایج را در قالب آرایهای از `OUTPUT`ها (بهصورت جنریک) برگرداند.
+ متد `OrderBy`: این متد با دریافت نام یک ستون و نحوهی ترتیب (`ASC` یا `DESC`، بهمعنای صعودی یا نزولی) باید یک بخش `ORDER BY` به کوئری اضافه کند.
+ متد `Where`: این متد میتواند حداقل دو آرگومان دریافت کند:
+ اگر دو آرگومان به این متد پاس داده شود، اولین آرگومان نام یک ستون و دومین آرگومان مقدار مدنظر خواهد بود. شرط برابر بودن مقدار ستون موردنظر با مقدار واردشده باید به کوئری اضافه شود. در صورتی که شرطی از قبل به کوئری اضافه شده باشد، شرط فعلی باید با شرطهای قبلی `AND` شود.
+ اگر سه آرگومان به این متد پاس داده شود، اولین آرگومان نام یک ستون، دومین آرگومان یک عملگر (نظیر `=`) و سومین آرگومان مقدار مدنظر خواهد بود. شرط واردشده باید به کوئری اضافه شود. در صورتی که شرطی از قبل به کوئری اضافه شده باشد، شرط فعلی باید با شرطهای قبلی `AND` شود.
+ اگر بیش از سه آرگومان به این متد پاس داده شود و آرگومان دوم برابر با رشتهی `IN` باشد، آرگومان اول برابر با نام ستون موردنظر خواهد بود. آرگومان سوم به بعد، مقادیری هستند که مقدار ستون موردنظر باید حداقل برابر با یکی از آنها باشد.
+ متد `WhereIn`: عملکرد این متد، مشابه حالت سوم متد `Where` است.
+ متد `AndWhere`: عملکرد این متد، مشابه متد `Where` است.
+ متد `OrWhere`: عملکرد این متد، مشابه متد `Where` است، با این تفاوت که اگر شرطی از قبل در کوئری موجود باشد، شرط جدید با شرطهای قبلی `OR` خواهد شد.
+ متد `Limit`: این متد با دریافت یک عدد، *limit* موجود در کوئری را مشخص میکند.
+ متد `Offset`: این متد با دریافت یک عدد، *offset* موجود در کوئری را مشخص میکند.
+ متد `Table`: این متد با دریافت نام جدول در قالب یک رشته، نام جدولی که کوئری باید روی آن اجرا شود را مشخص میکند.
+ متد `GroupBy`: این متد با دریافت تعداد نامشخصی رشته، یک عبارت *group by* به کوئری اضافه میکند. در صورتی که این متد چند بار فراخوانی شود، ستونها باید بهترتیب به بخش *group by* کوئری اضافه شوند.
+ متد `Select`: این متد با دریافت تعداد نامشخصی رشته، مشخص میکند که مقدار کدام ستونها از جدول دیتابیس دریافت شوند. اگر این متد فراخوانی نشود، همهی ستونها (`*`) باید از جدول موجود در دیتابیس دریافت شوند. اگر این متد چند بار فراخوانی شود، ستونها باید بهترتیب به کوئری اضافه شوند.
+ متد `SetDriver`: این متد با دریافت یک `Driver`، باید موارد موردنیاز برای کوئری بیلدر (نظیر `PlaceHolderGenerator`) را به موارد موجود در `Driver` تغییر دهد.
+ متد `ToSql`: این متد باید کوئری *SQL* تولیدشده را در قالب یک رشته و مقادیر پارامترهای موجود در کوئری را در قالب آرایهای از `interface{}`ها برگرداند. اگر اولین فراخوانی روی یک آبجکت جدید `QueryBuilder` مربوط به متد `ToSql` بود، مقدار خروجی `error` را برابر با یک رشتهی غیر از خالی قرار دهید. در غیر اینصورت، `error` را `nil` برگردانید.
**نکته:** در صورتی که چند شرط `AND` و `OR` شوند، صرفاً کافی است تا آنها را با استفاده از عملگرهای مربوطه (بین پرانتز) کنار یکدیگر قرار دهید. مثال:
```sql
WHERE cond1 OR cond2 AND cond3 OR cond4
```
کوئریهای تولیدشده توسط `QueryBuilder` باید مطابق با مثالهای زیر باشند (فاصله و کوچکی و بزرگی حروف مهم است):
### مثال ۱:
```go
s := orm.NewQueryBuilder[Dummy]()
s.Table("users")
s.ToSql()
```
کوئری ایجادشده:
```sql
SELECT * FROM users
```
### مثال ۲:
```go
s.Table("users").SetDriver(orm.Drivers.SQLite3).
Where("age", 10).
AndWhere("age", "<", 10).
Where("name", "CodeCup").
OrWhere("age", ">", 11)
.ToSql()
```
کوئری ایجادشده در درایور *SQLite3*:
```sql
SELECT * FROM users WHERE age = ? AND age < ? AND name = ? OR age > ?
```
### مثال ۳:
```go
orm.NewQueryBuilder[Dummy]().
SetDriver(orm.Drivers.PostgreSQL).
Table("users").
WhereIn("id", 1, 2, 3, 4, 5, 6).
ToSql()
```
کوئری ایجادشده در درایور *PostgreSQL*:
```sql
SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5, $6)
```
# نکات
+ توجه داشته باشید که در این *ORM* شما باید مستقیماً با دیتابیس در ارتباط باشید و امکان هندل کردن دادهها بهصورت *in-memory* را ندارید.
+ در صورت نیاز، میتوانید فایلهای جدیدی در برنامه تعریف کنید، اما امکان ایجاد دایرکتوری جدید را ندارید.
+ میتوانید از کتابخانههای *3rd party* در کد خود استفاده کنید. در اینصورت، باید فایلهای `go.mod` و `go.sum` را نیز در فایل زیپ ارسالی خود قرار دهید.
# آنچه باید آپلود کنید
پس از پیادهسازی برنامه، محتویات دایرکتوری اصلی برنامه را زیپ کرده و آپلود کنید، بهطوری که وقتی آن را باز میکنیم، با فایل `driver.go` و سایر فایلهای برنامه مواجه شویم.