PostgreSQL Tutorial Full Course 2022
I provide here in this PostgreSQL tutorial a full course you can use to master PostgreSQL. Postgres is an object relational database that is just as fast as MySQL that adheres more closely to SQL standards and excels at concurrency. Postgres is also superior at avoiding data corruption.
TABLE OF CONTENTS
00:00 Intro
00:30 Why Use Postgres?
01:13 What is a Database
03:12 Change Database Theme
03:53 Create a Database
04:46 Design a Database
05:50 Turn Invoice into a Database
07:04 Make a Table
12:13 Data Types
16:36 Adding Data to Table
18:15 To See Data
18:25 SELECT
19:19 Create Custom Type
20:48 Change Column Data Type
22:58 Thinking About Tables
25:37 Breaking Up Tables
27:03 Primary & Foreign Keys
32:40 Foreign & Primary Keys
33:28 Altering Tables Many Examples
53:00 Getting Data from One Table
53:40 Where
54:30 Conditional Operators
55:48 Logical Operators
58:12 Order By
59:32 Limit
1:01:45 GROUP BY
1:03:11 Distinct
1:05:00 Getting Data from Multiple Tables
1:05:21 Inner Join
1:08:50 Join 3 Tables
1:13:15 Arithmetic Operators
1:13:45 Join with Where
1:14:55 Outer Joins
1:17:03 Cross Joins
1:18:16 Unions
1:19:27 Extract
1:21:05 IS NULL
1:22:03 SIMILAR LIKE & ~
1:29:25 GROUP BY
1:31:14 HAVING
1:32:18 AGGREGATE FUNCTIONS
1:34:22 WORKING WITH VIEWS
1:45:01 SQL Functions
1:49:00 Dollar Quotes
1:50:06 Functions that Return Void
1:52:38 Get Maximum Product Price
1:53:39 Get Total Value of Inventory
1:54:26 Get Number of Customers
1:56:15 Named Parameters
2:01:30 Return a Row / Composite
2:03:38 Get Multiple Rows
2:07:08 PL/pgSQL
2:11:35 Variables in Functions
2:15:55 Store Rows in Variables
2:19:17 IN INOUT and OUT
2:21:01 Using Multiple Outs
2:25:56 Return Query Results
2:33:42 IF ELSEIF and ELSE
2:38:48 CASE Statement
2:42:01 Loop Statement
2:45:20 FOR LOOP
2:48:34 Result Sets, Blocks & Raise Notice
2:51:11 For Each and Arrays
2:53:20 While Loop
2:54:54 Continue
3:01:34 Stored Procedures
3:09:35 Triggers
3:29:25 Cursors
3:39:45 Installation
- «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid »
- «استفاده از Kendo UI templates»
صورت مساله
میخواهیم به یک چنین تصویری برسیم؛ که دارای گروه بندی اطلاعات است، فرمت شرطی روی ستون قیمت آن اعمال شده و تاریخ نمایش داده شده در آن نیز شمسی است. همچنین برای مثال ستون قیمت آن دارای ته جمع صفحه بوده و به علاوه یک دکمهی سفارشی به نوار ابزار آن اضافه شدهاست.
مباحث قسمت سمت سرور این مثال با مطلب «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid» دقیقا یکی است. فقط یک خاصیت AddDate نیز در اینجا اضافه شدهاست.
تغییر نحوهی نمایش pager
اگر به قسمت pager تصویر فوق دقت کنید، یک دکمهی refresh، تعداد موارد هر صفحه و امکان وارد کردن دستی شماره صفحه، در آن پیش بینی شدهاست. این موارد را با تنظیمات ذیل میتوان فعال کرد:
$("#report-grid").kendoGrid({ // ... pageable: { previousNext: true, // default true numeric: true, // default true buttonCount: 5, // default 10 refresh: true, // default false input: true, // default false pageSizes: true // default false },
بومی سازی پیغامهای گرید
پیغامهای فارسی را که در تصویر فوق مشاهده میکنید، حاصل پیوست فایل kendo.fa-IR.js هستند:
<!--https://github.com/loudenvier/kendo-global/blob/master/lang/kendo.fa-IR.js--> <script src="js/messages/kendo.fa-IR.js" type="text/javascript"></script>
گروه بندی اطلاعات
برای گروه بندی اطلاعات در Kendo UI Grid دو قسمت باید تغییر کنند.
ابتدا باید فیلد پیش فرض گروه بندی در قسمت data source گرید تعریف شود:
var productsDataSource = new kendo.data.DataSource({ // ... group: { field: "IsAvailable" }, // ... });
$("#report-grid").kendoGrid({ // ... groupable: true, // allows the user to alter what field the grid is grouped by // ...
اضافه کردن ته جمعهای ستونها
این ته جمعها که aggregate نام دارند باید در دو قسمت فعال شوند:
var productsDataSource = new kendo.data.DataSource({ //... aggregate: [ { field: "Name", aggregate: "count" }, { field: "Price", aggregate: "sum" } ] //... });
سپس این متدها را میتوان مطابق فرمت hash syntax قالبهای Kendo UI در قسمت footerTemplate هر ستون تعریف کرد:
$("#report-grid").kendoGrid({ // ... columns: [ { field: "Name", title: "نام محصول", footerTemplate: "تعداد: #=count#" }, { field: "Price", title: "قیمت", footerTemplate: "جمع: #=kendo.toString(sum,'c0')#" } ] // ... });
فرمت شرطی اطلاعات
در ستون قیمت، میخواهیم اگر قیمتی بیش از 2490 بود، با پس زمینهی قهوهای و رنگ زرد نمایش داده شود. برای این منظور میتوان یک قالب Kendo UI سفارشی را طراحی کرد:
<script type="text/x-kendo-template" id="priceTemplate"> #if( Price > 2490 ) {# <span style="background:brown; color:yellow;">#=kendo.toString(Price,'c0')#</span> #} else {# #= kendo.toString(Price,'c0')# #}# </script>
$("#report-grid").kendoGrid({ //... columns: [ { field: "Price", title: "قیمت", template: kendo.template($("#priceTemplate").html()), footerTemplate: "جمع: #=kendo.toString(sum,'c0')#" } ] //... });
فرمت تاریخ میلادی به شمسی در حین نمایش
برای تبدیل سمت کلاینت تاریخ میلادی به شمسی از کتابخانهی moment-jalaali.js کمک گرفته شدهاست:
<!--https://github.com/moment/moment/--> <script src="js/cultures/moment.min.js" type="text/javascript"></script> <!--https://github.com/jalaali/moment-jalaali--> <script src="js/cultures/moment-jalaali.js" type="text/javascript"></script>
$("#report-grid").kendoGrid({ //... columns: [ { field: "AddDate", title: "تاریخ ثبت", template: "#=moment(AddDate).format('jYYYY/jMM/jDD')#" } ] //... });
اضافه کردن یک دکمه به نوار ابزار گرید
نوار ابزار Kendo UI Grid را نیز میتوان توسط یک قالب سفارشی آن مقدار دهی کرد:
$("#report-grid").kendoGrid({ // ... toolbar: [ { template: kendo.template($("#toolbarTemplate").html()) } ] // ... });
<script> // این اطلاعات برای تهیه خروجی سمت سرور مناسب هستند function getCurrentGridFilters() { var dataSource = $("#report-grid").data("kendoGrid").dataSource; var gridState = { page: dataSource.page(), pageSize: dataSource.pageSize(), sort: dataSource.sort(), group: dataSource.group(), filter: dataSource.filter() }; return kendo.stringify(gridState); } </script> <script id="toolbarTemplate" type="text/x-kendo-template"> <a class="k-button" href="\#" onclick="alert('gridState: ' + getCurrentGridFilters());">نوار ابزار سفارشی</a> </script>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI05.zip
دریافت پارامترهای مسیریابی
گاهی از اوقات نیاز به ارسال پارامترهایی به مسیریابیهای تعریف شدهاست. برای مثال در لیست محصولات تعریف شده، بسته به انتخاب هر کدام، باید id متناظری نیز در URL نهایی ظاهر شود. به این Id، یک Route parameter گفته میشود. برای پیاده سازی این نیازمندی به صورت زیر عمل میکنیم:
در فایل app.js، یک مسیریابی جدید را برای نمایش جزئیات یک محصول اضافه میکنیم:
import ProductDetails from "./components/productDetails"; // ... class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> <Route path="/products/:id" component={ProductDetails} /> <Route path="/products" render={props => ( <Products param1="123" param2="456" {...props} /> )} /> <Route path="/posts/:year/:month" component={Posts} /> <Route path="/admin" component={Dashboard} /> <Route path="/" component={Home} /> </Switch> </div> </div> ); } }
کامپوننت جدید src\components\productDetails.jsx نیز به صورت زیر تعریف شدهاست:
import React, { Component } from "react"; class ProductDetails extends Component { handleSave = () => { // Navigate to /products }; render() { return ( <div> <h1>Product Details - </h1> <button className="btn btn-primary" onClick={this.handleSave}> </div> ); } } export default ProductDetails;
در اینجا با گشودن اطلاعات خاصیت match تزریق شدهی به کامپوننت ProductDetails، میتوان اطلاعاتی مانند پارامترهای دریافتی مسیریابی را دقیقا مشاهده کرد. برای مثال در این تصویر id=1 از URL بالای صفحه که به http://localhost:3000/products/1 تنظیم شدهاست، دریافت میشود.
بنابراین امکان خواندن اطلاعات پارامترهای مسیریابی، توسط شیء match تزریق شدهی به یک کامپوننت وجود دارد. به همین جهت کامپوننت ProductDetails را ویرایش کرده و المان h1 آنرا جهت نمایش id محصول به صورت زیر تغییر میدهیم که در آن شیء match.params، از props کامپوننت تامین میشود:
<h1>Product Details - {this.props.match.params.id} </h1>
برای آزمایش آن مجددا از صفحهی products شروع کرده و بر روی لینک یکی از محصولات، کلیک کنید. در اینجا هرچند id محصول به درستی نمایش داده میشود، اما ... نمایش جزئیات آن به همراه بارگذاری کامل و مجدد صفحهی آن است که از حالت SPA خارج شدهاست. برای رفع این مشکل به کامپوننت products مراجعه کرده و anchorهای تعریف شده را همانطور که در قسمت قبل نیز بررسی کردیم، تبدیل به کامپوننت Link میکنیم.
از حالت قبلی:
{this.state.products.map(product => ( <li key={product.id}> <a href={`/products/${product.id}`}>{product.name}</a> </li> ))}
import { Link } from "react-router-dom"; // ... <Link to={`/products/${product.id}`}>{product.name}</Link>
پارامترهای اختیاری مسیریابی
به تعریف مسیریابی زیر، دو پارامتر سال و ماه، اضافه شدهاند:
<Route path="/posts/:year/:month" component={Posts} />
برای رفع این مشکل میتوان با اضافه کردن یک ? به هر پارامتر، پارامترهای تعریف شده را اختیاری کرد:
<Route path="/posts/:year?/:month?" component={Posts} />
با این تغییرات اگر مجددا آدرس http://localhost:3000/posts/2018 را درخواست کنیم، کامپوننت Posts بجای کامپوننت Home نمایش داده میشود.
اکنون کامپوننت Posts را به صورت زیر تغییر میدهیم تا پارامترهای مسیریابی را نیز درج کند:
import React from "react"; const Posts = ({ match }) => { return ( <div> <h1>Posts</h1> Year: {match.params.year} , Month: {match.params.month} </div> ); }; export default Posts;
پس از ذخیره سازی این تغییرات و بارگذاری مجدد برنامه در مرورگر، اگر آدرس http://localhost:3000/posts/2018/1 را وارد کنیم، خروجی زیر حاصل میشود:
کار با پارامترهای کوئری استرینگهای مسیریابی
پارامترهای اختیاری، جزو قابلیتهایی هستند که باید تا حد ممکن از بکارگیری آنها اجتناب و آنها را با کوئری استرینگها تعریف کرد. کوئری استرینگها با یک ? در انتهای URL شروع میشوند و میتوانند چندین پارامتر را داشته باشند؛ مانند: http://localhost:3000/posts?sortBy=newest&approved=true و یا حتی میتوان آنها را با پارامترهای اختیاری نیز ترکیب کرد مانند: http://localhost:3000/posts/2018/05?sortBy=newest&approved=true
برای استخراج کوئری استرینگها در برنامههای React باید از شیء location استفاده کرد:
در اینجا مقدار خاصیت search، کل قسمت کوئری استرینگها را به همراه دارد. البته ما قصد پردازش آنرا به صورت دستی نداریم. به همین جهت از کتابخانهی زیر برای انجام اینکار استفاده خواهیم کرد:
> npm i query-string --save
import queryString from "query-string"; const Posts = ({ match, location }) => { const result = queryString.parse(location.search); console.log(result); // ...
- سپس شیء queryString را از ماژول مرتبط، import میکنیم. در ادامه به کمک متد parse آن، میتوان location.search را آنالیز کرد که خروجی آن، یک شیء جاوا اسکریپتی به صورت زیر است:
{approved: "true", sortBy: "newest"}
const { approved, sortBy } = queryString.parse(location.search);
یک نکته: باید دقت داشت که کتابخانهی query-string، همیشه مقادیر خواص را رشتهای بازگشت میدهد؛ حتی اگر عدد باشند.
مدیریت مسیرهای نامعتبر درخواستی
فرض کنید کاربری آدرس غیرمعتبر http://localhost:3000/xyz را که هیچ نوع مسیریابی را برای آن تعریف نکردهایم، درخواست میکند. در این حالت برنامه کامپوننت home را رندر میکند که مرتبط است با تعاریف مسیریابی برنامه در فایل app.js. تنها path تعریف شدهای که با این آدرس تطابق پیدا میکند، / متناظر با کامپوننت home است.
بجای این رفتار پیشفرض، مایل هستیم که کاربر به یک صفحهی سفارشی «پیدا نشد» هدایت شود. به همین جهت ابتدا کامپوننت جدید تابعی بدون حالت src\components\notFound.jsx را با محتوای زیر ایجاد میکنیم:
import React from "react"; const NotFound = () => { return <h1>Not Found</h1>; }; export default NotFound;
<Route path="/" exact component={Home} />
- ابتدا شیء Redirect را از react-router-dom باید import کنیم.
- همچنین import کامپوننت NotFound نیز باید ذکر شود.
- سپس پیش از مسیریابی کلی /، مسیریابی جدید not-found را که به کامپوننت NotFound اشاره میکند، اضافه میکنیم.
- اکنون در انتهای Switch تعریف شده (جائی که دیگر هیچ مسیریابی تعریف شدهای، با مسیر درخواستی کاربر، تطابق نداشته)، باید کامپوننت Redirect را جهت هدایت به این مسیریابی جدید، تعریف کرد:
import { Redirect, Route, Switch } from "react-router-dom"; //... import NotFound from "./components/notFound"; //... class App extends Component { render() { return ( <div> <NavBar /> <div className="container"> <Switch> //... <Route path="/not-found" component={NotFound} /> <Route path="/" exact component={Home} /> <Redirect to="/not-found" /> </Switch> </div> </div> ); } }
کاربرد دیگر کامپوننت Redirect، هدایت کاربران از یک آدرس قدیمی، به یک آدرس جدید است که نحوهی تعریف آن به صورت زیر میباشد:
<Redirect from="/messages" to="/posts" />
هدایت کاربران به آدرسهای مختلف با برنامه نویسی
گاهی از اوقات پس از تکمیل فرمی و یا کلیک بر روی دکمهای، میخواهیم کاربر را به آدرس خاصی هدایت کنیم. برای مثال در برنامهی جاری میخواهیم زمانیکه کاربری صفحهی جزئیات یک محصول را مشاهده و بر روی دکمهی فرضی Save کلیک کرد، دوباره به همان صفحهی لیست محصولات هدایت شود؛ برای این منظور از شیء history استفاده خواهیم کرد:
در اینجا متدها و خواص شیء history را مشاهده میکنید. برای نمونه توسط متد push آن میتوان آدرس جدیدی را به تاریخچهی آدرسهای مرور شدهی توسط کاربر، اضافه کرد و کاربر را با برنامه نویسی، به صفحهی جدیدی هدایت نمود:
class ProductDetails extends Component { handleSave = () => { // Navigate to /products this.props.history.push("/products"); };
یک نکته: اگر به تصویر دقت کنید، متد replace هم در اینجا قابل استفاده است. متد push با افزودن رکوردی به تاریخچهی آدرسهای مرور شدهی در مرورگر، امکان بازگشت به محل قبلی را با کلیک بر روی دکمهی back مرورگر، فراهم میکند؛ اما replace تنها رکورد آدرس جاری را در تاریخچهی مرورگر به روز رسانی میکند. یعنی از داشتن تاریخچه محروم خواهیم شد. عمدهی کاربرد این متد، در صفحات لاگین است؛ زمانیکه کاربر به سیستم وارد میشود و به صفحهی جدیدی مراجعه میکند، با کلیک بر روی دکمهی back، دوباره نمیخواهیم او را به صفحهی لاگین هدایت کنیم.
تعریف مسیریابیهای تو در تو
قصد داریم به صفحهی admin، دو لینک جدید به مطالب و کاربران ادمین را اضافه کنیم، به نحوی که با کلیک بر روی هر کدام، محتوای هر صفحهی متناظر، در همینجا نمایش داده شود (تصویر فوق). به عبارتی در حال حاضر، مسیریابی تعریف شده، جهت مدیریت لینکهای NavBar بالای صفحه کار میکند. اکنون میخواهیم مسیریابی دیگری را داخل آن برای پوشش منوی کنار صفحهی ادمین اضافه کنیم. به اینکار، تعریف مسیریابیهای تو در تو گفته میشود و پیاده سازی آن توسط کامپوننت react-router-dom بسیار سادهاست و شامل این موارد میشود:
- ابتدا مسیریابیهای جدید را همینجا داخل کامپوننت src\components\admin\dashboard.jsx تعریف میکنیم:
const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> <div className="row"> <div className="col-3"> <SideBar /> </div> <div className="col"> <Route path="/admin/users" component={Users} /> <Route path="/admin/posts" component={Posts} /> </div> </div> </div> ); };
تنها نکتهی جدید آن، امکان درج کامپوننت Route در قسمتهای مختلف برنامه، خارج از app.js میباشد و این امکان محدود به app.js نیست. در این حالت اگر مسیر /admin/posts توسط کاربر وارد شد، دقیقا در همان محلی که کامپوننت Route درج شدهاست، کامپوننت متناظر با این مسیر یعنی کامپوننت Posts، رندر میشود.
در ادامه محتوای این کامپوننتهای جدید را نیز ملاحظه میکنید:
محتوای کامپوننت src\components\admin\sidebar.jsx
import React from "react"; import { Link } from "react-router-dom"; const SideBar = () => { return ( <ul className="list-group"> <li className="list-group-item"> <Link to="/admin/posts">Posts</Link> </li> <li className="list-group-item"> <Link to="/admin/users">Users</Link> </li> </ul> ); }; export default SideBar;
محتوای کامپوننت src\components\admin\users.jsx
import React from "react"; const Users = () => { return <h1>Admin Users</h1>; }; export default Users;
محتوای کامپوننت src\components\admin\posts.jsx
import React from "react"; const Posts = () => { return ( <div> <h1>Admin Posts</h1> </div> ); }; export default Posts;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-15-part-02.zip
MongoDB #7
- String: این نوع پرکاربردترین نوع داده برای ذخیره اطلاعات است. رشته در MongoDB باید بصورت یونیکد (utf-8) معتبر باشد.
- Integer: این نوع برای ذخیره کردن یک مقدار عددی استفاده میشود. Integer بسته به نوع سرور میتواند 32 یا 64 بیت باشد.
- Boolean: این نوع برای ذخیره کردن یک مقدار بولی (true/false) استفاده میشود.
- Double: این نوع برای مقادیر با ممیز شناور استفاده میشود.
- کلیدهای Min/Max: این نوع برای مقایسه یک مقدار با کمترین یا بیشترین عناصر BSON استفاده میشود.
- Array: این نوع برای ذخیره آرایهها یا لیست یا چندین مقدار در یک کلید استفاده میشود.
- Timestamp: این نوع میتواند برای ضبط زمان تغییرات (مثلا وقتی یک سند درج میشود یا تغییر میکند) مفید باشد.
- Object: این نوع برای سندهای توکار استفاده میشود.
- Null: این نوع برای ذخیره مقدار تهی (Null) استفاده میشود.
- Symbol: این نوع بطور یکسان برای ذخیره رشته استفاده میشود، اما عموما برای زبانهایی که از یک نوع نماد (Symbol) مشخص استفاده میکنند تعبیه شده است.
- Date: این نوع برای ذخیره تاریخ یا زمان جاری به فرمت زمان در یونیکس (UNIX) استفاده میشود. با ساخت یک شی از نوع Date و ارسال روز، ماه و سال به آن میتوانید تاریخ مشخص خود را داشته باشید.
- Object ID: ای نوع برای ذخیره سازی شناسه سند استفاه میشود.
- Binary Data: این نوع برای ذخیره سازی داده باینری استفاده میشود.
- Code: این نوع برای ذخیره سازی کد جاوا اسکریپت داخل سند استفاده میشود.
- Regular Expression: این نوع برای ذخیره سازی عبارت باقاعده استفاده میشود.
درج سند در MongoDB
>db.COLLECTION_NAME.insert(document)
>db.mycol.insert({ _id: ObjectId(7df78ad8902c), title: 'MongoDB Overview', description: 'MongoDB is no sql database', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 })
>db.post.insert([ { title: 'MongoDB Overview', description: 'MongoDB is no sql database', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 }, { title: 'NoSQL Database', description: 'NoSQL database doesn't have tables', by: 'tutorials point', url: 'http://www.tutorialspoint.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 20, comments: [ { user:'user1', message: 'My first comment', dateCreated: new Date(2013,11,10,2,35), like: 0 } ] } ])
ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاقها از Web API
در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاقها و یا یک اتاق مشخص به کلاینت است. اکنون میخواهیم در ادامهی قسمت قبل، اگر کاربری بر روی دکمهی Go صفحهی اول رزرو اتاقی کلیک کرد، لیست تمام اتاقهای تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
namespace BlazorWasm.Client.Services { public interface IClientHotelRoomService { public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate); public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate); } }
در ادامه اینترفیس فوق را به صورت زیر پیاده سازی میکنیم:
namespace BlazorWasm.Client.Services { public class ClientHotelRoomService : IClientHotelRoomService { private readonly HttpClient _httpClient; public ClientHotelRoomService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate) { throw new NotImplementedException(); } public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, "/api/hotelroom")) .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}") .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}") .Uri; return _httpClient.GetFromJsonAsync<IEnumerable<HotelRoomDTO>>(uri); } } }
- HttpClient یکی از سرویسهای تنظیم شدهی در فایل Program.cs پروژههای سمت کلاینت است. بنابراین میتوان آنرا از طریق تزریق به سازندهی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شدهاست. این مورد جزو تازههای NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده میکنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده میکنید:
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; using BlazorServer.Models; using BlazorWasm.Client.Utils; using System; using System.Collections.Specialized; using System.Web; namespace BlazorWasm.Client.Utils { public class UriBuilderExt { private readonly NameValueCollection _collection; private readonly UriBuilder _builder; public UriBuilderExt(Uri uri) { _builder = new UriBuilder(uri); _collection = HttpUtility.ParseQueryString(string.Empty); } public UriBuilderExt AddParameter(string key, string value) { _collection.Add(key, value); return this; } public Uri Uri { get { _builder.Query = _collection.ToString(); return _builder.Uri; } } } }
- تاریخهای ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالبهای پذیرفته شدهاست.
- جهت سهولت استفادهی از سرویس فوق و همچنین مدلهای برنامه، فضای نام آنها را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامهی سمت کلاینت، قابل دسترسی شوند:
@using BlazorWasm.Client.Services @using BlazorServer.Models
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientHotelRoomService, ClientHotelRoomService>(); // ... } } }
چند اصلاح جزئی در کنترلرها و سرویسهای سمت سرور
در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]")] [ApiController] public class HotelRoomController : ControllerBase { // ... [HttpGet] public async Task<IActionResult> GetHotelRooms(DateTime? checkInDate, DateTime? checkOutDate) { // ... } [HttpGet("{roomId}")] public async Task<IActionResult> GetHotelRoom(int? roomId, DateTime? checkInDate, DateTime? checkOutDate) { // ... } } }
namespace BlazorServer.Services { public interface IHotelRoomService : IDisposable { Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDate, DateTime? checkOutDate); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate); // ... } }
تنظیمات ویژهی HttpClient برنامهی سمت کلاینت
سرویس ClientHotelRoomService فوق، از HttpClient تزریق شدهی در سازندهی خود استفاده میکند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت میکند. در اینجا علاقمندیم تا بجای این تنظیم پیشفرض، فایل جدید appsettings.json را به پوشهی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامههای سمت کلاینت، داخل پوشهی wwwroot است و نه در داخل پوشهی ریشهی اصلی پروژه):
{ "BaseAPIUrl": "https://localhost:5001/" }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl")) }); // ... } } }
تکمیل کامپوننت دریافت لیست تمام اتاقها
در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمهی Go صفحهی اول، به این کامپوننت هدایت میشوند. اکنون میخواهیم، لیست تمام اتاقها را در این کامپوننت، از Web API برنامه دریافت کنیم:
@page "/hotel/rooms" @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime @inject IClientHotelRoomService HotelRoomService <h3>HotelRooms</h3> @code { HomeVM HomeModel = new HomeVM(); IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { try { var model = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking); if (model is not null) { HomeModel = model; } else { HomeModel.NoOfNights = 1; } await LoadRooms(); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } private async Task LoadRooms() { Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate); } }
روش اجرای پروژههای Blazor WASM
تا اینجا اگر برنامهی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحهی خالی نمایش لیست اتاقها ظاهر میشود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژهی Web API را اجرا کرد و سپس پروژهی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده میکنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینهی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینهی «multiple startup projects» را انتخاب کرده و از لیست پروژههای موجود، دو پروژهی Web API و WASM را انتخاب کنید و Action مقابل آنها را به Start تنظیم کنید. در اینجا حتی میتوان ترتیب اجرای این پروژهها را هم تغییر داد. در این حالت زمانیکه بر روی دکمهی Run، در ویژوال استودیو کلیک میکنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.
نکتهی مهم! در این حالت هم برنامهی کلاینت نمیتواند با برنامهی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شدهی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژهی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
{ "iisSettings": { "iisExpress": { "applicationUrl": "http://localhost:62930", "sslPort": 44314 } } }
{ "BlazorWasm.Client": { "applicationUrl": "https://localhost:5002;http://localhost:5003", } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-27.zip
در این قسمت بر روی توابع Topcount, bottomcount , toppercent, bottompercent, topsum, bottomsum تمرکز خواهیم داشت.
در ابتدا تصور کنید بخواهیم میزان فروش اینترنتی را برای پنج ردیف از دسته بندیهای محصولات واکشی کنیم.
Select [Measures].[Internet Sales Amount] on columns, non empty( topcount([Product].[Product Categories].[Subcategory],5) ) on rows From [Adventure Works]
در تابع بالا پنج ردیف ابتدایی (به صورت فیزیکی) برگردانده میشوند.
در اینجا تابع topcount دارای دو پارامتر می باشد که پارامتر دوم آن مشخص کنندهی تعداد ردیف واکشی شده و پارامتر اول آن، مشخص کنندهی دایمنشنی میباشد که عمل واکشی برای آن صورت میگیرد. همچنین در بالا از تابع Non empty برای حذف ردیفهای دارای مقدار Null استفاده شده است. حال تصور کنید بخواهیم پنج دسته بندی محصولی را دریافت کنیم که دارای بیشترین میزان فروش اینترنتی میباشند.
Select [Measures].[Internet Sales Amount] on columns, non empty( topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Internet Sales Amount] ) ) on rows From [Adventure Works]
خروجی بر اساس میزان فروش اینترنتی به صورت نزولی مرتب شده است.
تابع Topcount به عنوان پارامتر سوم میتواند نام یک Measure را دریافت کند و خروجی را براساس آن شاخص، برگرداند. امکان واکشی و مرتب سازی در تابع Topcount
برای یک شاخص متفاوت از شاخص واکشی شده در یک محور دیگر نیز وجود دارد به مثال زیر دقت کنید:
Select [Measures].[Internet Sales Amount] on columns, topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Reseller Sales Amount] ) on rows From [Adventure Works]
همانطور که مشخص میباشد، پنج دسته بندی محصولاتی که دارای بیشترین میزان فروش نمایندگان فروش میباشند، در خروجی واکشی شدهاند؛ در حالیکه در محور ستون میزان فروش اینترنتی واکشی شده است.
برای درک بیشتر همین کوئری را دوباره بازنویسی کرده اما اینبار در محور ستون هر دو شاخص [Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount] را واکشی میکنیم.
Select {[Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount]} on columns, topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Reseller Sales Amount] ) on rows From [Adventure Works]
با بررسی خروجی دو کوئری بالا تفاوت واکشی را متوجه خواهید شد. در هر دو کوئری واکشی براساس شاخص [Measures].[Reseller Sales Amount] انجام شده است
اما واکشی در محور ستون متفاوت میباشد. (دقیقا مانند T/SQL که میتوانستیم، مرتب سازی براساس فیلدی باشد که در قسمت Projection حاضر نبوده و در این حالت در برخی موارد ظاهرا خروجی مرتب نمیباشد)
حال تصور کنید بخواهیم 30 دسته بندی محصولاتی را داشته باشیم که دارای کمترین میزان فروش اینترنتی میباشند. برای این منظور از تابع bottomcount استفاده میکنیم
Select [Measures].[Internet Sales Amount] on columns, bottomcount( [Product].[Product Categories].[Subcategory], 30, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
ردیف هایی که دارای مقدار Null می باشند هم در خروجی قرار می گیرند
Select [Measures].[Internet Sales Amount] on columns, non empty bottomcount( [Product].[Product Categories].[Subcategory], 30, [Measures].[Internet Sales Amount] )on rows From [Adventure Works]
در مثال بالا ردیفهای دارای مقدار Null را از خروجی حذف کرده ایم.
گاهی نیاز میباشد که تعداد دسته بندیهای محصولاتی را واکشی کنیم که دارای بیشترین یا کمترین میزان فروش اینترنتی میباشند و سرجمع فروش اینترنتی آنها بیشتر یا کمتر از X درصد از فروش اینترنتی کل میباشد را داشته باشند. به عنوان مثال میخواهیم ببینیم کدام دسته بندی محصولات شامل بیشترین میزان فروش اینترنتی میباشند و سرجمع فروش آنها 53 در صد از کل فروش اینترنتی میباشند.
Select [Measures].[Internet Sales Amount] on columns, { toppercent( [Product].[Product Categories].[Subcategory], 53, [Measures].[Internet Sales Amount] ), [Product].[Product Categories] } on rows From [Adventure Works]
و یا واکشی دسته محصولاتی که دارای کمترین میزان فروش اینترنتی میباشند و سرجمع فروش اینترنتی آنها کمتر از 1 درصد کل میزان فروش اینترنتی میباشد.
Select [Measures].[Internet Sales Amount] on columns, non empty bottompercent( [Product].[Product Categories].[Subcategory], --0.01, 1, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
کاربرد تابع Topsum در کوئری زیر نمایش داده شده است
Select [Measures].[Internet Sales Amount] on columns, topsum( [Product].[Product Categories].[Subcategory], 25000000, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
در این کوئری از تابع TopSum استفاده شده است که عملا حداکثر تعداد دسته بندی محصولاتی را بازیابی میکند که دارای بیشترین میزان فروش بوده اند و همچنین در مجموع بیش از 25000000 فروش داشته باشند .
تابع bottomsum عملا تعداد دسته بندی محصولاتی را که دارای کمترین میزان فروش بوده اند و همچنین سرجمع میزان فروش اینترنتی آنها 100000 بوده است را بر می گرداند. البته خروجی توسط non empty ، فیلتر شده است و خروجی هایی که کاملا Null می باشند، حذف گردیده اند.
Select [Measures].[Internet Sales Amount] on columns, non empty bottomsum( [Product].[Product Categories].[Subcategory], 100000, [Measures].[Internet Sales Amount] )on rows From [Adventure Works]
تزریق وابستگیها در فیلترهای ASP.NET MVC
public class StructureMapGlobalFilterProvider : IFilterProvider { public StructureMapGlobalFilterProvider(IContainer container, GlobalFilterRegistrationList filterList) { _container = container; _filterList = filterList; } private IContainer _container; private GlobalFilterRegistrationList _filterList; public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { var filters = new List<Filter>(); if (_filterList == null || _filterList.Count == 0) return filters; foreach (GlobalFilterRegistration registration in _filterList) { var actionFilter = _container.GetInstance(registration.Type); var filter = new Filter(actionFilter, FilterScope.Global, registration.Order); filters.Add(filter); } return filters; } } public class GlobalFilterRegistration { public Type Type { get; set; } public int? Order { get; set; } } public class GlobalFilterRegistrationList : List<GlobalFilterRegistration> { }
و تنظیمات Global:
var globalFilterRegistrationList = new GlobalFilterRegistrationList { new GlobalFilterRegistration { Type = typeof (LogAttribute), Order = 1 } }; container.Configure(x => { x.For<IFilterProvider>().Use<StructureMapGlobalFilterProvider>(); x.For<GlobalFilterRegistrationList>().Use(globalFilterRegistrationList); });
ایجاد دکمه های دلخواه Bootstrap ای
مروری بر کاربردهای Action و Func - قسمت اول
Action<int,int,int> ac;
مفهوم Workspace در Angular
/projects apps .....applications..... libs .....libraries.....