Doctrine多实体标签关联:含三列的ManyToMany关系实现咨询
Since your join table includes an extra entity_name column to distinguish between different entity types, a standard JPA @ManyToMany won’t work—it only supports two foreign keys. Instead, we’ll use a join entity (Tagging) to model this three-column association. This approach gives you full control over the join table while keeping your entities clean.
Step 1: Create the Tag Entity
This is your core tag entity with built-in category support:
@Entity @Table(name = "tags") public class Tag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String name; private String category; // For your categorized vocabulary // Bidirectional link to Tagging entities @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) private List<Tagging> taggings = new ArrayList<>(); // Constructors public Tag() {} public Tag(String name, String category) { this.name = name; this.category = category; } // Convenience methods to manage taggings public void addTagging(Tagging tagging) { taggings.add(tagging); tagging.setTag(this); } public void removeTagging(Tagging tagging) { taggings.remove(tagging); tagging.setTag(null); } // Getters and setters // ... }
Step 2: Create the Composite Key for Tagging
Your join table uses a composite primary key (tag_id, entity_name, record_id), so we’ll define an embeddable ID class:
@Embeddable public class TaggingId implements Serializable { private Long tagId; private String entityName; // Stores canonical class name (e.g., "com.example.Article") private Long recordId; // Stores the ID of the tagged entity instance // Constructors public TaggingId() {} public TaggingId(Long tagId, String entityName, Long recordId) { this.tagId = tagId; this.entityName = entityName; this.recordId = recordId; } // Equals and hashCode are REQUIRED for composite keys @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TaggingId that = (TaggingId) o; return Objects.equals(tagId, that.tagId) && Objects.equals(entityName, that.entityName) && Objects.equals(recordId, that.recordId); } @Override public int hashCode() { return Objects.hash(tagId, entityName, recordId); } // Getters and setters // ... }
Using canonical class names avoids conflicts if you have classes with the same name in different packages.
Step 3: Create the Tagging Join Entity
This entity models your three-column join table and links a Tag to a specific entity instance:
@Entity @Table(name = "tag_association") // Replace with your actual join table name public class Tagging { @EmbeddedId private TaggingId id; // Link to the Tag entity (maps to tagId in TaggingId) @ManyToOne(fetch = FetchType.LAZY) @MapsId("tagId") private Tag tag; // Constructors public Tagging() {} // Helper constructor to create a Tagging for a taggable entity public Tagging(Tag tag, Taggable entity) { this.id = new TaggingId( tag.getId(), entity.getClass().getCanonicalName(), entity.getId() ); this.tag = tag; } // Getters and setters public TaggingId getId() { return id; } public void setId(TaggingId id) { this.id = id; } public Tag getTag() { return tag; } public void setTag(Tag tag) { this.tag = tag; } }
Step 4: Define a Taggable Interface
To standardize access to entity IDs across all taggable entities, create a simple interface:
public interface Taggable { Long getId(); }
Step 5: Add Tagging Support to Your Entities
For any entity that needs tagging (e.g., Article, Product), implement the Taggable interface and add a collection of Tagging entities:
@Entity @Table(name = "articles") public class Article implements Taggable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String content; // Link to Tagging entities filtered by this entity type @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumnsOrFormulas({ @JoinColumnOrFormula(column = @JoinColumn(name = "record_id", referencedColumnName = "id")), @JoinColumnOrFormula(formula = @JoinFormula( value = "'" + Article.class.getCanonicalName() + "'", referencedColumnName = "entity_name" )) }) private List<Tagging> taggings = new ArrayList<>(); // Convenience methods to add/remove tags (hides Tagging entity from clients) public void addTag(Tag tag) { Tagging tagging = new Tagging(tag, this); taggings.add(tagging); tag.addTagging(tagging); } public void removeTag(Tag tag) { Tagging taggingToRemove = taggings.stream() .filter(t -> t.getId().getTagId().equals(tag.getId())) .findFirst() .orElse(null); if (taggingToRemove != null) { taggings.remove(taggingToRemove); tag.removeTagging(taggingToRemove); } } // Helper method to get all tags for this article public List<Tag> getTags() { return taggings.stream() .map(Tagging::getTag) .collect(Collectors.toList()); } // Implement Taggable interface @Override public Long getId() { return id; } // Getters and setters for other fields // ... }
The @JoinColumnsOrFormulas annotation ensures we only fetch Tagging records where entity_name matches the canonical name of Article and record_id matches the article’s ID.
Step 6: Querying Tags for an Entity
To fetch tags for a specific entity instance, use the getTags() convenience method, or create a repository query:
@Repository public interface TagRepository extends JpaRepository<Tag, Long> { @Query("SELECT t FROM Tag t JOIN t.taggings tagg " + "WHERE tagg.id.entityName = :entityName AND tagg.id.recordId = :recordId") List<Tag> findTagsByEntityAndId( @Param("entityName") String entityName, @Param("recordId") Long recordId ); }
Usage example:
List<Tag> articleTags = tagRepository.findTagsByEntityAndId( Article.class.getCanonicalName(), article.getId() );
Key Notes
- Why You Need
TaggingEntity: JPA’s standard@ManyToManydoesn’t support extra columns in the join table. TheTaggingentity lets you model the three-column association explicitly. - Canonical Class Names: Using
getCanonicalName()ensures unique entity identifiers even if you have classes with the same name in different packages. - Bidirectional Relationships: The convenience methods (
addTag,removeTag) maintain both sides of the bidirectional relationship betweenTagandTagging, preventing orphaned records.
内容的提问来源于stack exchange,提问作者Daimos

