← Назад к вопросам

Может ли Entity быть абстрактным классом?

2.3 Middle🔥 101 комментариев
#ORM и Hibernate

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Entity как абстрактный класс

Короткий ответ

Да, может, и часто должна быть! Абстрактные классы для entities - это отличный способ избежать дублирования кода и реализовать полиморфизм в ORM (Hibernate, JPA).

Когда нужен абстрактный Entity?

Сценарий: Иерархия сущностей

// ✅ Абстрактный базовый класс с общими полями
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "entity_type")
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @Column(nullable = false)
    private LocalDateTime updatedAt;
    
    private String createdBy;
    private String updatedBy;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now(UTC);
        updatedAt = LocalDateTime.now(UTC);
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now(UTC);
    }
    
    public Long getId() { return id; }
    public LocalDateTime getCreatedAt() { return createdAt; }
}

// Конкретные entities наследуют от базового
@Entity
@Table(name = "users")
public class User extends BaseEntity {
    @Column(unique = true, nullable = false)
    private String email;
    
    private String firstName;
    private String lastName;
    
    // Получает id, createdAt, updatedAt, createdBy, updatedBy от BaseEntity
}

@Entity
@Table(name = "products")
public class Product extends BaseEntity {
    @Column(nullable = false)
    private String name;
    
    @Column(columnDefinition = "TEXT")
    private String description;
    
    private BigDecimal price;
    
    // Также получает все поля от BaseEntity
}

Стратегии наследования в Hibernate

1. SINGLE_TABLE (рекомендуется для простых случаев)

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Payment {
    @Id
    @GeneratedValue
    private Long id;
    
    private BigDecimal amount;
    private LocalDateTime createdAt;
}

@Entity
@DiscriminatorValue("CREDIT_CARD")
public class CreditCardPayment extends Payment {
    private String cardNumber;
    private String cardHolder;
}

@Entity
@DiscriminatorValue("PAYPAL")
public class PayPalPayment extends Payment {
    private String email;
}

// SQL результат:
// CREATE TABLE payment (
//   id BIGINT PRIMARY KEY,
//   amount DECIMAL,
//   created_at TIMESTAMP,
//   payment_type VARCHAR(31),
//   card_number VARCHAR,
//   card_holder VARCHAR,
//   email VARCHAR
// );

// Плюсы: простая структура, быстрые запросы
// Минусы: nullable поля, может быть много столбцов

2. JOINED (рекомендуется для сложных иерархий)

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "entity_type")
public abstract class Animal {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
}

@Entity
@Table(name = "dogs")
@DiscriminatorValue("DOG")
public class Dog extends Animal {
    private String breed;
    
    @ManyToOne
    private Owner owner;
}

@Entity
@Table(name = "cats")
@DiscriminatorValue("CAT")
public class Cat extends Animal {
    private int livesRemaining;
}

// SQL результат:
// CREATE TABLE animal (
//   id BIGINT PRIMARY KEY,
//   name VARCHAR,
//   entity_type VARCHAR
// );
// 
// CREATE TABLE dogs (
//   id BIGINT PRIMARY KEY REFERENCES animal(id),
//   breed VARCHAR,
//   owner_id BIGINT
// );
// 
// CREATE TABLE cats (
//   id BIGINT PRIMARY KEY REFERENCES animal(id),
//   lives_remaining INT
// );

// Плюсы: нормализованная структура, не nullable
// Минусы: join при каждом запросе

3. TABLE_PER_CLASS (избегай!)

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
    
    private String manufacturer;
}

@Entity
@Table(name = "cars")
public class Car extends Vehicle {
    private int doors;
}

@Entity
@Table(name = "motorcycles")
public class Motorcycle extends Vehicle {
    private boolean hasSidecar;
}

// Плюсы: каждый класс имеет отдельную таблицу
// Минусы: UNION для запросов, сложные миграции

Реальный пример: Иерархия документов

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "doc_type")
public abstract class Document {
    @Id
    @GeneratedValue
    private Long id;
    
    @Column(nullable = false)
    private String title;
    
    @Column(columnDefinition = "TEXT")
    private String content;
    
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @Column(nullable = false)
    private LocalDateTime updatedAt;
    
    @Enumerated(EnumType.STRING)
    private DocumentStatus status;
    
    @ManyToOne
    private User author;
    
    // Abstract методы для полиморфизма
    public abstract void validate();
    public abstract String getFormattedContent();
    
    // Общие методы
    public boolean isDraft() {
        return status == DocumentStatus.DRAFT;
    }
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now(UTC);
        updatedAt = LocalDateTime.now(UTC);
        status = DocumentStatus.DRAFT;
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now(UTC);
    }
}

@Entity
@Table(name = "blog_posts")
@DiscriminatorValue("BLOG_POST")
public class BlogPost extends Document {
    @ElementCollection
    private List<String> tags;
    
    private Integer viewCount;
    
    @Override
    public void validate() {
        if (getTitle() == null || getTitle().isEmpty()) {
            throw new IllegalArgumentException("Title is required");
        }
        if (getTags().isEmpty()) {
            throw new IllegalArgumentException("At least one tag is required");
        }
    }
    
    @Override
    public String getFormattedContent() {
        return String.format("# %s\\n\\n%s\\n\\nTags: %s",
            getTitle(),
            getContent(),
            String.join(", ", tags)
        );
    }
}

@Entity
@Table(name = "reports")
@DiscriminatorValue("REPORT")
public class Report extends Document {
    @Enumerated(EnumType.STRING)
    private ReportType reportType;
    
    @OneToMany(mappedBy = "report", cascade = CascadeType.ALL)
    private List<ReportSection> sections;
    
    @Override
    public void validate() {
        if (getSections().isEmpty()) {
            throw new IllegalArgumentException("Report must have sections");
        }
    }
    
    @Override
    public String getFormattedContent() {
        return sections.stream()
            .map(ReportSection::getContent)
            .collect(Collectors.joining("\\n\\n"));
    }
}

// Использование с полиморфизмом
@Service
public class DocumentService {
    @Autowired
    private DocumentRepository documentRepository;
    
    // Получаем все документы (включая BlogPost и Report)
    public List<Document> getAllDocuments() {
        return documentRepository.findAll();
    }
    
    // Полиморфный вызов
    public void publishDocument(Long id) {
        Document doc = documentRepository.findById(id)
            .orElseThrow(() -> new NotFoundException("Document not found"));
        
        // Validate вызывает правильный метод для BlogPost или Report
        doc.validate();
        
        // Сохраняем в PDF с правильным форматированием
        String formatted = doc.getFormattedContent();
        saveToPdf(formatted);
    }
}

Best Practices

✅ Делай абстрактным, если:

  • Есть общие поля в нескольких entities (id, createdAt, updatedAt)
  • Есть общая бизнес-логика (@PrePersist, @PreUpdate)
  • Нужен полиморфизм (разные типы одного объекта)
  • Хочешь избежать дублирования кода

❌ Избегай абстрактных entities, если:

  • Это просто один класс, без наследников
  • Не нужна общая функциональность
  • Иерархия очень глубокая (> 3 уровней)

Рекомендуемая структура:

┌─────────────────────────┐
│   @MappedSuperclass     │
│    BaseEntity           │
├─────────────────────────┤
│ - id                    │
│ - createdAt             │
│ - updatedAt             │
└──────────────────────────┘
    △       △       △
    │       │       │
 ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
 │User │ │Post │ │Order │
 └─────┘ └─────┘ └─────┘

Вариант 2 (с полиморфизмом):

┌─────────────────────────┐
│  @Entity (abstract)     │
│  Document               │
└─────────────────────────┘
    △       △
    │       │
┌──┴──┐ ┌──┴──┐
│Blog │ │Report│
 └────┘ └─────┘

Вывод

Да, Entity может и должна быть абстрактным классом! Это часть хорошей архитектуры ORM-приложений. Используй абстрактные классы для:

  • Общих полей (id, timestamps)
  • Общей бизнес-логики (lifecycle callbacks)
  • Иерархий сущностей (полиморфизм)

Выбери правильную стратегию наследования (JOINED или SINGLE_TABLE в большинстве случаев) и оптимизируй свою схему БД.

Может ли Entity быть абстрактным классом? | PrepBro