در این بخش به مفهوم Excerpts و Projection میپردازیم. از Projection برای ایجاد View های سفارشی از Model هایمان و چگونه از Excerpt ها به عنوان View های پیش فرض در جمع آوری Resource استفاده کنیم
مفهوم Projection :
همانطور که پیش تر در این پست به آن اشاره شده در حالت عادی وقتی یک entity را از طریق ORM دریافت میکنیم شامل کلیه Column هایی است که در هر ردیف موجود است در صورتی که ممکن است نیازی به همه آن اطلاعات نداشته باشیم مفهموم Projection به دریافت Column های مورد نظر از جدول گفته میشود که باعث صرفه جویی در منابع میشود
مفهوم Excerpts :
یک Excerpt یک Projection است که اتوماتیک روی مجموعه ای از Resource اعمال میشود و عبارت است از یک پیش نمایش پیش فرض مجموعه ای از دیتا که از Resource های مختلفی جمع آوری شده است . باید دقت کرد که Excerpt ها بصورت اتوماتیک روی یک Resource اعمال نمیشوند و اگر بخواهیم باید اینکار را عمدا انجام دهیم
Domain Model های زیر را در نظر بگیرید :
@Entity public class Book { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private long id; @Column(nullable = false) private String title; private String isbn; @ManyToMany(mappedBy = "books", fetch = FetchType.EAGER) private List<Author> authors; }
@Entity public class Author { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private long id; @Column(nullable = false) private String name; @ManyToMany(cascade = CascadeType.ALL) @JoinTable( name = "book_author", joinColumns = @JoinColumn( name = "book_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn( name = "author_id", referencedColumnName = "id")) private List<Book> books; }
که بینشان رابطه Many To Many برقرار است یعنی یک کتاب میتواند تعدادی نویسنده داشته باشد و یک نویسنده میتواند تعدادی کتاب داشته باشد
و برای هر یک از Model ها یک Repository در نظر بگیرید :
public interface BookRepository extends CrudRepository<Book, Long> {}
public interface AuthorRepository extends CrudRepository<Author, Long> {}
و ما میتوانیم با ارسال id یک کتاب اطلاعات آن کتاب را داشته باشیم
http://localhost:8080/books/{id}
{ "title" : "Animal Farm", "isbn" : "978-1943138425", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1" }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } }
از آنجا که Author ها Repository خودشان را دارند جزییات Author را Response قابل مشاهده نیست اما طبق تعریفی که کردیم میتوانیم با لینک http://localhost:8080/books/1/authors به آنها دسترسی داشته باشیم
حال بیاییم برای حل این مسئله یک Projection طراحی کنیم :
گاهی ما به برخی از فیلد های یک entity نیاز داریم که در این صورت یک Projection طراحی میکنیم
Projection :
یک Projection طراحی میکنیم با نام CustomBook:
@Projection( name = "customBook", types = { Book.class }) public interface CustomBook { String getTitle(); }
* طراحی Projection ها شامل یک اینترفیس است که Projection@ دارد که میتواند نام دلخواه داشته باشد که ما customBook به آن داده ایم و نوع ابجکت مربوط به آنرا که نیاز به فیلد های آن دارد را تعیین میکنیم . در اینجا Projection ما تنهای نیاز به title کتاب ها دارد و متد getter آنرا نیز ایجاد میکنیم
حالا بیاییم بعد از اضافه شدن Projection به Response اطلاعات Book دوباره نگاهی بکنیم :
{ "title" : "Animal Farm", "isbn" : "978-1943138425", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1{?projection}", "templated" : true }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } }
همانطور که میبینیم یک لینک برای Projection ایجاد شده است
حالا میاییم یک درخواست ارسال میکنیم تا title کتاب را دریافت کنیم :
http://localhost:8080/books/1?projection=customBook
{ "title" : "Animal Farm", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1{?projection}", "templated" : true }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } }
طبق الگوی {http://localhost:8080/books/1?projection={projection name دسترسی به Projection امکان پذیر شد و title کتاب با id =1 را دریافت کردیم
* دقت کنید که Projection باید در همان پکیجی که کلاس های Model قرار گرفته اند تعریف شوند در غیر اینصورت باید آنها را به عنوان Projection به Spring توسط کلاس RepositoryRestConfigurerAdapter بشناسانیم :
@Configuration public class RestConfig extends RepositoryRestConfigurerAdapter { @Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration repositoryRestConfiguration) { repositoryRestConfiguration.getProjectionConfiguration() .addProjection(CustomBook.class); } }
اضافه کردن دیتای جدید به Projection :
مثلا دریافت اطلاعاتی که در View اصلی پنهان است مانند Id کتاب ها
@Projection( name = "customBook", types = { Book.class }) public interface CustomBook { @Value("#{target.id}") long getId(); String getTitle(); }
حال اگر دوباره درخواست بدهیم Id کتاب اضافه شده است :
{ "id" : 1, "title" : "Animal Farm", "_links" : { ... } }
* دقت داشته باشید که با Projection حتی فیلد هایی که JsonIgnore@ شده باشند نیز قابل دریافت هستند
ساخت دیتاهای محاسباتی در Projection :
مثلا میخواهیم تعداد نویسنده هر کتاب را داشته باشیم :
@Projection(name = "customBook", types = { Book.class }) public interface CustomBook { @Value("#{target.id}") long getId(); String getTitle(); @Value("#{target.getAuthors().size()}") int getAuthorCount(); }
و اگر درخواست را دوباره ارسال کنیم :
{ "id" : 1, "title" : "Animal Farm", "authorCount" : 1, "_links" : { ... } }
اگر به اطلاعات اضافی دیگری که مرتبط بود نیاز داشتیم میتوانیم انرا صراحتا بیاوریم و از ارسال درخواست اضافی جلوگیری کنیم :
@Projection( name = "customBook", types = { Book.class }) public interface CustomBook { @Value("#{target.id}") long getId(); String getTitle(); List<Author> getAuthors(); @Value("#{target.getAuthors().size()}") int getAuthorCount(); }
{ "id" : 1, "title" : "Animal Farm", "authors" : [ { "name" : "George Orwell" } ], "authorCount" : 1, "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1{?projection}", "templated" : true }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } }
Excerpts :
Except ها در واقع چندین Projection هستند که به عنوان Default View برای مجموعه Resource ها استفاده میکنیم
ابتدا می آییم BookRepository را سفارشی میکنیم تا از customBook Projecton برای جمع آوری دیتا استفاده کند برای این منظور از ویژگی excerptProjection در RepositoryRestResource@ استفاده میکنیم :
@RepositoryRestResource(excerptProjection = CustomBook.class) public interface BookRepository extends CrudRepository<Book, Long> {}
اکنون میتوان مطمئن شد که customBook به عنوان View پیش فرض در http://localhost:8080/books میباشد
{ "_embedded" : { "books" : [ { "id" : 1, "title" : "Animal Farm", "authors" : [ { "name" : "George Orwell" } ], "authorCount" : 1, "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1{?projection}", "templated" : true }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/books" }, "profile" : { "href" : "http://localhost:8080/profile/books" } } }
همینطور روی View نویسنده ها اعمال میشود :
http://localhost:8080/authors/1/books
{ "_embedded" : { "books" : [ { "id" : 1, "authors" : [ { "name" : "George Orwell" } ], "authorCount" : 1, "title" : "Animal Farm", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1{?projection}", "templated" : true }, "authors" : { "href" : "http://localhost:8080/books/1/authors" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/authors/1/books" } } }
* همانطور که گفته شد از Excerpts برای جمع اوری اتوماتیک مجموعه ای از دیتا استفاده میشود و اگر نیاز به فیلد هایی از یک کلاس بود از Projection استفاده میکنیم و اگر Projection ها را به عنوان Default View مجموعه ها در نظر بگیریم نحوه آپدیت کردن یک منبع از بین منابع دشوار خواهد شد
** به عنوان نکته پایانی باید یادمان باشد که استفاده از Projection و Excerpt برای مقاصد فقط-خواندنی اطلاعات استفاده میشوند