টোরাকোড ডেভেলপার্স নেটওয়ার্ক

HIbernate Many to Many - কিভাবে বিদ্যমান কলামের সাথে লিংক করা যায়?

hibernate
jpa

(Muhammad Nezam Uddin) #1

আমি মূলত বিগিনার। স্পৃং ফ্রেইমওর্য়াক শিখছি।
আমার অ্যাপ এ দুইটা টেবিল আছে। পার্সন ও ক্যাটাগরি। এবং এদের মাঝে রিলেশনশিপ বিদ্যমান। একজন পার্সন এর একাধিক ক্যাটাগরি থাকতে পারে, আবার এক ক্যাটাগরি একাধিক পার্সনকে হোল্ড করতে পারে।
এখানে ক্লাসগুলোর কোড দিলাম।

@Entity
@Table(name = "person")
public class Person implements Serializable {


    private Integer id;
    private String name;
    private String address;
    private String imageUrl;
    private List<Affiliation> affiliation;
    private List<Investment> investmentList;
    private List<Category> categories;

    public Person() {
    }

    public Person(String name, String address, String imageUrl) {
        this.name = name;
        this.address = address;
        this.imageUrl = imageUrl;
        affiliation = new ArrayList<>();
        investmentList = new ArrayList<>();
        categories=new ArrayList<>();
    }

    public Person(List<Category> categories) {
        this.categories = categories;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(name = "person_name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(name = "person_address")
    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Column(name = "image_url")
    public String getImageUrl() {
        return imageUrl;
    }

    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
    public List<Affiliation> getAffiliation() {
        return affiliation;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Integer getId() {
        return id;
    }

    public void setAffiliation(List<Affiliation> affiliation) {
        this.affiliation = affiliation;
    }

    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
    public List<Investment> getInvestmentList() {
        return investmentList;
    }

    public void setInvestmentList(List<Investment> investmentList) {
        this.investmentList = investmentList;
    }

    @ManyToMany(cascade = {CascadeType.PERSIST,CascadeType.MERGE})
    @JoinTable(name = "person_category", joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "category_id", referencedColumnName = "id"))
    public List<Category> getCategories() {
        return categories;
    }

    public void setCategories(List<Category> categories) {
        this.categories = categories;
    }
}
@Entity
@Table(name = "category")
public class Category implements Serializable {

    private int id;
    private String categoryName;

    public Category() {
    }

    public Category(String categoryName) {
        this.categoryName = categoryName;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Column(name = "category_name", nullable = false)
    public String getCategoryName() {
        return categoryName;
    }

    public void setCategoryName(String categoryName) {
        this.categoryName = categoryName;
    }
}

সমস্যা হলো, থাইমলীফের মাধ্যমে তৈরি করা ভিউ থেকে সহজেই নতুন পার্সন এন্ট্রি দিতে পারছি। কিন্তু প্রতিটা নতুন পার্সন এন্ট্রি দেয়ার সময় একই নামে নতুন নতুন ক্যাটাগরি তৈরি হচ্ছে। নতুন পার্সন তৈরি হবার সময় ক্যাটাগরি আছে কি না সেটা চেক করার একটা উপায় দরকার। এজন্য কন্ট্রোলারে এরকম কিছু কোড যোগ করলাম।

 @PostMapping(value = "/addBusiness")
    public String createBusiness(@ModelAttribute Business business, @RequestParam("file") MultipartFile file) {
        
        List<Category> checkList = business.getCategories();
        List<Category>newList=new ArrayList<>();

        for (int i=0;i<checkList.size();i++) {
            ExampleMatcher NAME_MATCHER = ExampleMatcher.matching()
                    .withMatcher("categoryName", ExampleMatcher.GenericPropertyMatchers.ignoreCase());
            Example<Category> example = Example.<Category>of(new Category(checkList.get(i).getCategoryName()),
                                            NAME_MATCHER);
            if (categoryRepo.exists(example)){
                Category linkCat=categoryRepo.findOne(example).get();
                checkList.add(i,linkCat);
            }
        }
        business.setCategories(checkList);
        businessRepo.save(business);
        return "/add-business";
    }

কিন্তু এরকম একটা এরর পাচ্ছি।

Field error in object 'person' on field 'categories': rejected value [entrepreneur,investor]; codes [typeMismatch.person.categories,typeMismatch.categories,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.categories,categories]; arguments []; default message [categories]]; default message [Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.List' for property 'categories'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value 'entrepreneur'; nested exception is java.lang.NumberFormatException: For input string: "entrepreneur"]

আমার প্রশ্ন: যেভাবে বিদ্যমান রেকর্ড চেক করছি সেটা যথাযথ কি না?

এত লম্বা প্রশ্ন পড়ার এবং সাহায্যের জন্য আগাম ধন্যবাদ


(Sayem Hossain) #2

দেরিতে উত্তর দেয়ার জন্য দুঃখিত নিজাম ভাই। কাজের ব্যাস্ততায় সময় বের করা কঠিন হয়ে যায়, তবে আরো আগে উত্তর দেয়া উচিৎ ছিল, এজন্য দুঃখপ্রকাশ করছি।

আপনি যে রিলেশনশিপের কথা এখানে উল্লেখ করেছেন, সেটা একটি many-to-many রিলেশনশিপ। এই রিলেশনশিপের জন্য আপনার আলাদা একটি টেবিলের দরকার হবে যেটা হাইবারনেট অটোমেটিকলি তৈরি করে। তবে আপনাকে জেপিএ/হাইবারনেটকে জানিয়ে দিতে হবে যে এটা একটা many-to-many রিলেশনশিপ।

এখানে আপনি Person টেবিলে private List<Category> categories; রেখেছেন কিন্তু তার উপরে @ManyToMany অ্যানোটেশন দেন নি।

কালেকশন ইন্সট্যান্সের উপরে কোনরূপ অ্যানোটেশন না থাকলে হাইবারনেট ডিফল্টভাবে @ElementCollection হিসেবে ধরে নেয়। @ManyToMany এবং @ElementCollection এর মধ্যে পার্থক্য হচ্ছে, @ElementCollection নন এনটিটি অবজেক্টের ক্ষেত্রে ব্যাবহার করা হয় যেখানে নতুন একটি টেবিল তৈরি হবে নন এনটিটি অবজেক্টের জন্য এবং Foreign Key হিসেবে প্যারেন্ট এনটিটির আইডি থাকবে।

অন্যদিকে, @ManyToMany রিলেশনের ক্ষেত্রে দুটি অবজেক্টের জন্যই ভিন্ন টেবিল থাকবে এবং একটি নতুন টেবিল হবে যেখানে দুটি টেবলের প্রাইমারি কি নতুন টেবিলের ফরিন কি হিসেবে নিজেদের মধ্যে রিলেশন ঠিক করবে।

এখন আপনার প্রব্লেমে আসি। আপনার ক্ষেত্রে যেটা ঘটছে সেটা হচ্ছে, প্রতিবার পারসন যুক্ত করার সাথে সাথে নতুন ক্যাটেগরিও তৈরি হচ্ছে কারণ হাইবারনেট এটাকে একটা কালেকশন ভেবে নিচ্ছে। @ManyToMany এর ক্ষেত্রে প্রথমে ক্যাটেগরির আইডি খুঁজবে। আইডি পেলে সে আইডির ক্যাটেগরি এই পারসনের সাথে রিলেশন তৈরি করবে নতুন ক্যাটেগরি তৈরি করবে না। ক্যাটেগরি আইডি নাল হলে নতুন ক্যাটেগরি তৈরি করবে।

এভাবে করুন আশা করি সমস্যার সমাধান হবে।

	class Person{
		@ManyToMany(cascade=CascadeType.ALL)
		List<Category> categories;
	}

আপনার json ডেটার স্ট্র্যাকচার এভাবে করা উচিত।

{
"name":"your name",
"categories": [
	{
		"id" : 1
	}
]

}


(Muhammad Nezam Uddin) #3

ধন্যবাদ সায়েম ভাই। দেরি হতেই পারে, কোন ব্যাপার না।

আমি মূলত Filed লেভেলে এনোটেশন না দিয়ে মেথড লেভেলে দিয়েছি। আমার দেয়া কোডে স্ক্রল করলে হয়তো দেখতে পাবেন।

আমার সমস্যার কারণ, যেটা আপনি বলেছেন, সেটাই। Thymeleaf টেম্পলেটে আমি ক্যাটাগরি লিস্টটা হার্ডকোড করে দিয়েছিলাম। সেখানে ক্যাটাগরি আইডি টা ছিল না। যার কারণে হাইবারনেট নতুন কালেকশন ধরে নিয়ে নতুন নতুন ক্যাটাগরি তৈরি করছিল। আমি যেটা করেছি, Form এ ক্যাটাগরি তালিকা ডেটাবেজ থেকে দেখানোর ব্যবস্থা করেছি। এতে হাইবারনেট ক্যাটাগরি গুলো চিনতে পেরেছে। আর আমার সমস্যাও সলভ হয়েছে।

আর ElementCollection বিষয়টা জানা ছিল না, জানা হলো। এজন্য আবারো ধন্যবাদ।