Javaにおけるシングルテーブル継承パターン:統一されたテーブル構造によるオブジェクトマッピングの効率化
別名
- クラステーブル継承
- STI
シングルテーブル継承デザインパターンの目的
シングルテーブル継承パターンは、Javaアプリケーションにおけるデータベーススキーマを簡素化します。
異なるクラスのオブジェクトを表す行と、すべての属性の集合を表す列を用いて、継承階層を単一のデータベーステーブルに効率的に格納します。
現実世界の例を用いたシングルテーブル継承パターンの詳細な説明
現実世界の例
図書館システムを想像してみてください。書籍、雑誌、DVDはすべて単一のインベントリテーブルに格納されています。このテーブルには、`Title`、`Author`、`PublicationDate`、`ItemType`など、すべてのアイテムに共通する属性の列と、書籍の`ISBN`、雑誌の`IssueNumber`、DVDの`Duration`など、特定の種類に固有の列が含まれています。各行は図書館内のアイテムを表し、アイテムの種類によってはいくつかの列がNULLになります。この設定は、すべてのインベントリアイテムを1つのテーブルに保持することでデータベーススキーマを簡素化し、データベースコンテキストにおけるシングルテーブル継承に似ています。
簡単に言うと
シングルテーブル継承は、型の識別子列を使用して異なるサブクラスを区別することで、クラス階層全体を単一のデータベーステーブルに格納します。
Wikipediaによると
シングルテーブル継承は、リレーショナルデータベースでオブジェクト指向の継承をエミュレートする方法です。オブジェクト指向言語のオブジェクトにデータベーステーブルをマッピングする際に、データベース内のフィールドによって、オブジェクトが階層内のどのクラスに属するかが識別されます。「シングルテーブル継承」という名前が示すように、すべてのクラスのすべてのフィールドが同じテーブルに格納されます。
Javaにおけるシングルテーブル継承パターンのプログラミング例
シングルテーブル継承は、クラスの継承階層を単一のデータベーステーブルにマッピングするデザインパターンです。テーブルの各行は、階層内のクラスのインスタンスを表します。特別な識別子列を使用して、各行が属するクラスを識別します。
このパターンは、継承階層内のクラスがフィールドと動作の点で大きく異ならない場合に役立ちます。データベーススキーマを簡素化し、結合を回避することでパフォーマンスを向上させることができます。
このパターンが提供されたコードでどのように実装されているか見てみましょう。
階層の基底クラスは`Vehicle`です。このクラスは、すべての車両が共有する共通のプロパティ(`manufacturer`と`model`など)を持っています。
public abstract class Vehicle {
private String manufacturer;
private String model;
// other common properties...
}
`Vehicle`を拡張し、追加のプロパティを追加する2つのサブクラス`PassengerVehicle`と`TransportVehicle`があります。
public abstract class PassengerVehicle extends Vehicle {
private int noOfPassengers;
// other properties specific to passenger vehicles...
}
public abstract class TransportVehicle extends Vehicle {
private int loadCapacity;
// other properties specific to transport vehicles...
}
最後に、`PassengerVehicle`と`TransportVehicle`をそれぞれ拡張する`Car`と`Truck`のような具体的なクラスがあります。
public class Car extends PassengerVehicle {
private int trunkCapacity;
// other properties specific to cars...
}
public class Truck extends TransportVehicle {
private int towingCapacity;
// other properties specific to trucks...
}
この例では、ORMとしてHibernateを使用しています。`@Entity`および`@Inheritance`アノテーションを使用して、クラス階層を単一のテーブルにマッピングします。
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Vehicle {
// properties...
}
`@DiscriminatorColumn`アノテーションを使用して、テーブル内の識別子列を指定します。この列には、各行のクラス名が格納されます。
@DiscriminatorColumn(name = "vehicle_type")
public abstract class Vehicle {
// properties...
}
各サブクラスは、`@DiscriminatorValue`アノテーションを使用して識別子値を指定します。
@DiscriminatorValue("CAR")
public class Car extends PassengerVehicle {
// properties...
}
@DiscriminatorValue("TRUCK")
public class Truck extends TransportVehicle {
// properties...
}
`VehicleService`クラスは、車両の保存と取得を行うメソッドを提供します。`Car`または`Truck`オブジェクトを保存すると、Hibernateは識別子列に適切な値を自動的に設定します。車両を取得すると、Hibernateは識別子列を使用して正しいクラスをインスタンス化します。
public class VehicleService {
public Vehicle saveVehicle(Vehicle vehicle) {
// save vehicle to database...
}
public Vehicle getVehicle(Long id) {
// retrieve vehicle from database...
}
public List<Vehicle> getAllVehicles() {
// retrieve all vehicles from database...
}
}
最後に、この例を実行するSpring Bootアプリケーションを示します。
@SpringBootApplication
@AllArgsConstructor
public class SingleTableInheritance implements CommandLineRunner {
//Autowiring the VehicleService class to execute the business logic methods
private final VehicleService vehicleService;
public static void main(String[] args) {
SpringApplication.run(SingleTableInheritance.class, args);
}
@Override
public void run(String... args) {
Logger log = LoggerFactory.getLogger(SingleTableInheritance.class);
log.info("Saving Vehicles :- ");
// Saving Car to DB as a Vehicle
Vehicle vehicle1 = new Car("Tesla", "Model S", 4, 825);
Vehicle car1 = vehicleService.saveVehicle(vehicle1);
log.info("Vehicle 1 saved : {}", car1);
// Saving Truck to DB as a Vehicle
Vehicle vehicle2 = new Truck("Ford", "F-150", 3325, 14000);
Vehicle truck1 = vehicleService.saveVehicle(vehicle2);
log.info("Vehicle 2 saved : {}\n", truck1);
log.info("Fetching Vehicles :- ");
// Fetching the Car from DB
Car savedCar1 = (Car) vehicleService.getVehicle(vehicle1.getVehicleId());
log.info("Fetching Car1 from DB : {}", savedCar1);
// Fetching the Truck from DB
Truck savedTruck1 = (Truck) vehicleService.getVehicle(vehicle2.getVehicleId());
log.info("Fetching Truck1 from DB : {}\n", savedTruck1);
log.info("Fetching All Vehicles :- ");
// Fetching the Vehicles present in the DB
List<Vehicle> allVehiclesFromDb = vehicleService.getAllVehicles();
allVehiclesFromDb.forEach(s -> log.info(s.toString()));
}
}
コンソール出力
2024-05-27T12:29:49.949+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Starting SingleTableInheritance using Java 17.0.4.1 with PID 56372 (/Users/ilkka.seppala/git/java-design-patterns/single-table-inheritance/target/classes started by ilkka.seppala in /Users/ilkka.seppala/git/java-design-patterns)
2024-05-27T12:29:49.951+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : No active profile set, falling back to 1 default profile: "default"
2024-05-27T12:29:50.154+03:00 INFO 56372 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-05-27T12:29:50.176+03:00 INFO 56372 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 19 ms. Found 1 JPA repository interface.
2024-05-27T12:29:50.315+03:00 INFO 56372 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-05-27T12:29:50.345+03:00 INFO 56372 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final
2024-05-27T12:29:50.360+03:00 INFO 56372 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-05-27T12:29:50.457+03:00 INFO 56372 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-05-27T12:29:50.468+03:00 INFO 56372 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2024-05-27T12:29:50.541+03:00 INFO 56372 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:sti user=SA
2024-05-27T12:29:50.542+03:00 INFO 56372 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-05-27T12:29:50.930+03:00 INFO 56372 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-05-27T12:29:50.953+03:00 INFO 56372 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-05-27T12:29:51.094+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Started SingleTableInheritance in 1.435 seconds (process running for 1.678)
2024-05-27T12:29:51.095+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Saving Vehicles :-
2024-05-27T12:29:51.114+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Vehicle 1 saved : Car{PassengerVehicle(noOfPassengers=4)}
2024-05-27T12:29:51.115+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Vehicle 2 saved : Truck{ TransportVehicle(loadCapacity=3325), towingCapacity=14000}
2024-05-27T12:29:51.115+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Fetching Vehicles :-
2024-05-27T12:29:51.129+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Fetching Car1 from DB : Car{PassengerVehicle(noOfPassengers=0)}
2024-05-27T12:29:51.130+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Fetching Truck1 from DB : Truck{ TransportVehicle(loadCapacity=0), towingCapacity=14000}
2024-05-27T12:29:51.130+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Fetching All Vehicles :-
2024-05-27T12:29:51.169+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Car{PassengerVehicle(noOfPassengers=0)}
2024-05-27T12:29:51.169+03:00 INFO 56372 --- [ main] com.iluwatar.SingleTableInheritance : Truck{ TransportVehicle(loadCapacity=0), towingCapacity=14000}
2024-05-27T12:29:51.172+03:00 INFO 56372 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-05-27T12:29:51.173+03:00 INFO 56372 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-05-27T12:29:51.174+03:00 INFO 56372 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
シングルテーブル継承パターンは、継承階層をリレーショナルデータベースにマッピングするシンプルで効率的な方法です。ただし、サブクラスに多くの固有のフィールドがある場合、スパーステーブルにつながる可能性があります。そのような場合は、クラステーブル継承やコンクリートテーブル継承などの他のパターンの方が適している可能性があります。
Javaでシングルテーブル継承パターンを使用するケース
- 共通の基底クラスを共有するサブクラスを持つクラス階層があり、階層のすべてのインスタンスを単一のテーブルに格納する場合に使用します。
- 単一のテーブルのシンプルさが、一部のサブクラスのNULLフィールドのパフォーマンスコストを上回る、中規模アプリケーションに最適です。
シングルテーブル継承パターンのチュートリアル
Javaにおけるシングルテーブル継承パターンの現実世界の応用事例
- JavaアプリケーションにおけるHibernateとJPAの実装では、多くの場合、ORMマッピングにシングルテーブル継承が使用されます。
- Rails ActiveRecordは、シングルテーブル継承を標準でサポートしています。
シングルテーブル継承パターンのメリットとデメリット
メリット
Java ORMにおけるシングルテーブル継承パターンの使用
- テーブル数を減らすことでデータベーススキーマを簡素化します。
- すべてのデータが1つのテーブルにあるため、関係とクエリの管理が容易になります。
デメリット
- 多くのNULL値を持つスパーステーブルにつながる可能性があります。
- テーブルサイズと型のフィルタリングの必要性により、大規模な階層ではパフォーマンスの問題が発生する可能性があります。
- 継承階層の変更にはスキーマの変更が必要です。
関連するJavaデザインパターン
- クラステーブル継承:階層内の各クラスに個別のテーブルを使用し、NULL値を減らしながらも結合の複雑さを増加させます。
- コンクリートテーブル継承:階層内の各クラスに独自のテーブルがあり、冗長性を減らしながらもテーブル数を増加させます。