1. Bài toán
Giả sử ta có 1 bảng question và question_tag quan hệ nhiều nhiều với nhau thông qua bảng trung gian question_question_tags.
Cấu trúc từng bảng như sau:
Cấu trúc từng bảng như sau:
create_table "questions", force: :cascade do |t| t.string "question", null: false t.boolean "require_flg", null: false t.integer "question_type", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "question_tags", force: :cascade do |t| t.string "tag_name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["tag_name"], name: "index_question_tags_on_tag_name", unique: true end create_table "question_question_tags", force: :cascade do |t| t.integer "question_id", null: false t.integer "question_tag_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["question_id"], name: "index_question_question_tags_on_question_id" t.index ["question_tag_id"], name: "index_question_question_tags_on_question_tag_id" end
Trong đó question_tags.tag_name không được trùng nhau và question_tags, question_question_tags sẽ được tạo cùng với questions
2. Vấn đề
Nếu như dùng nested attribute thông thường mỗi khi tạo 1 bản ghi question với question_tag thì sẽ tạo mới toàn bộ question_tag
VD: tạo mới 1 bản ghi question với 3 question_tag. Question tag có tag_name lần lượt là 'a', 'b', 'c'
Trong db lúc này đã có sẵn 2 bản ghi question tag với tag_name lần lượt là 'a', 'b'
Như vậy expect là sẽ tạo 2 bản ghi question_question_tag liên kết question mới tạo và 2 bản ghi question_tag đã có sẵn trong db ('a', 'b')
và tạo mới 1 bản ghi question_tag (tag_name = 'c') và liên kết tiếp nó với question
Nhưng thực tế rails sẽ cố tạo ra 3 bản ghi question_tag mới với tag_name = 'a', 'b', 'c'. Như vậy sẽ bị lỗi do bản question_tag đã có 2 record tag_name = 'a' và 'b' rồi
VD: tạo mới 1 bản ghi question với 3 question_tag. Question tag có tag_name lần lượt là 'a', 'b', 'c'
Trong db lúc này đã có sẵn 2 bản ghi question tag với tag_name lần lượt là 'a', 'b'
Như vậy expect là sẽ tạo 2 bản ghi question_question_tag liên kết question mới tạo và 2 bản ghi question_tag đã có sẵn trong db ('a', 'b')
và tạo mới 1 bản ghi question_tag (tag_name = 'c') và liên kết tiếp nó với question
Nhưng thực tế rails sẽ cố tạo ra 3 bản ghi question_tag mới với tag_name = 'a', 'b', 'c'. Như vậy sẽ bị lỗi do bản question_tag đã có 2 record tag_name = 'a' và 'b' rồi
3. Giải quyết
Đoạn này không thể dùng default của nested attribute được mà sẽ phải xử lý viết tay trong model
Ta viết chèn vào hàm question_tags_attributes= đây là hàm mà nested attribute sẽ gọi vào khi tạo model thông thường có bao nhiêu bản ghi question_tag mà không có gắn id thì nó sẽ gán thêm cho self.question_tags bấy nhiêu bản ghi mới. Ta sửa lại bằng cách check trước trong db xem đã có bản ghi với tag_name trùng chưa nếu có thì đẩy bản ghi đó vào self.question_tags nếu chưa có thì tạo 1 bản ghi new với attribute người dùng truyền lên rồi đẩy vào self.question_tags. Các attributes có _destroy sẽ bị lọc ra, chỉ cần bản ghi không có trong self.question_tags nested attribute sẽ xóa nó cho chúng ta
Detail code ở dưới đây:
Ta viết chèn vào hàm question_tags_attributes= đây là hàm mà nested attribute sẽ gọi vào khi tạo model thông thường có bao nhiêu bản ghi question_tag mà không có gắn id thì nó sẽ gán thêm cho self.question_tags bấy nhiêu bản ghi mới. Ta sửa lại bằng cách check trước trong db xem đã có bản ghi với tag_name trùng chưa nếu có thì đẩy bản ghi đó vào self.question_tags nếu chưa có thì tạo 1 bản ghi new với attribute người dùng truyền lên rồi đẩy vào self.question_tags. Các attributes có _destroy sẽ bị lọc ra, chỉ cần bản ghi không có trong self.question_tags nested attribute sẽ xóa nó cho chúng ta
Detail code ở dưới đây:
class Question < ApplicationRecord ... def question_tags_attributes=(attributes) self.question_tags = BuildQuestionTagsAttributesService.run(attributes) end end
class BuildQuestionTagsAttributesService < BaseService def initialize(attributes) @attributes = attributes end def run return [] if attributes.blank? tag_names = attributes.map do |_, attribute| attribute[:tag_name] end question_tags = get_question_tags(tag_names) question_tag_attributes = [] attributes.each do |_, attribute| next if attribute[:_destroy] == 'true' || attribute[:_destroy] == '1' question_tag = question_tags.find do |qt| qt.tag_name == attribute[:tag_name] end question_tag = QuestionTag.new(attribute.except(:_destroy)) if question_tag.blank? question_tag_attributes << question_tag end question_tag_attributes end private attr_reader :attributes def get_question_tags(tag_names) QuestionTag.where(tag_name: tag_names) end end