You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 58 Next »

JPA의 컨셉은 Java Persistence API 의 약자로 데이터 베이스에 존재하는 모델을

자바객체로 DB종속적이지 않게 맵핑을 가능하게 하는데 목적이 있습니다.

JPA의 대략적인 사용법을 소개하고, 하위 섹션에서 기존 SQL 개발방식이 JPA에서는 어떻게

효율적인 방법으로 구현하는지를 살펴보면서, JPA와 별개로 RDB를 잘 활용하는 방법은

RDB를 잘 이해하는것임으로 JPA와 같이 정리하도록 하겠습니다.


설명을 위해, 축약된 코드로 설명을 하였으며, 풀소스는 아래에서 확인가능합니다.

CodeLink : http://git.webnori.com/projects/WEBF/repos/spring_jpa/browse

JPA 개념 추가설명


접속 DB 설정

# application.properties
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=test
spring.datasource.password=test1234

spring.jpa.hibernate.ddl-auto 옵션

  • none: 테이블 구조 변경에 관여하지 않지않기때문에, DB 스키마와 JPA 모델을 맞추어놓아야합니다.
  • update: JPA에서 정의한 데이터모델과,  데이터베이스의 스키마 변경이 있을때 반영됩니다. 

  • create: 어플리케이션 구동시 drop+create 를 수행

  • create-drop: 어플리케이션 구동시 drop+create를 수행하고 종료시 drop수행

  • validate : 테이블과 엔티티매핑정보가 맞지않으면 어플리케이션 실행하지 않음


네이밍룰

JPA에서 class를 통해 테이블을 정의할때 몇가지 규칙이 있습니다.

항목DBJPA객체설명
Table(Class)sample_tableSampleTable헝가리 표기법을따름
Field(member)sample_namesampleName카멜표기법을 따름




db가 설치된 os에따라 대소문자를 구분하는 경우가 있기때문에

JPA가 핸들링하는 DB에서의 네이밍은 모두 소문자로 셋팅이됩니다.

만약 네이밍규칙을 다르게하고싶으면, Class명 혹은 필드명 바로위에 아래와같은 어노테이션을 사용합니다.

  • @Table(name=”SAMPLE_TABLE”)
  • @Column(name = “COLUMN_NAME”,nullable=false) 


전역 네이밍룰을 변경하고싶으면

application.properties:


  • spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.EJB3NamingStrategy
  • spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl


Data Model(Entity) 생성

User Entity
package com.psmon.springdb;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity // This tells Hibernate to make a table out of this class
public class User {
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Integer id;
	
	private String name;
	
	private String email;

	public Integer getId() {
		return id;
	}

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

	public String getName() {
		return name;
	}

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

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

}


실제 DataBase에서는 위와같은 테이블이, 어플리케이션 시작시 자동 생성됩니다.



CRUD 저장소생성

 전통적인 DataBase를 통한 개발방법은,  SQL을 직접이용하거나, 주로 SP를 이용하여 Table의 정보를 읽거나 변경을 하였습니다.

JPA에서는 CrudRepository를 이용하여 조금더 객체 지향접근방식을 통해 Database를 제어할수가 있습니다.

User테이블을 제어하는 객체를 정의하기 위해서, 여기서는 UserRepository 라고 정의를 하였습니다.

기본적인 조회기능/업데이트/삭제 기능만 사용한다고 하면, 이 코드에서 추가해야할 코드가 없어도됩니다.

상속박은 CrudRepository에 이미 CreateReadUpdateDelete와 관련된 바로 사용가능한 
기본 인터페이스가 정의되어있습니다.


UserRepository
package com.psmon.springdb;

import org.springframework.data.repository.CrudRepository;

//This will be AUTO IMPLEMENTED by Spring into a Bean called userRepository
//CRUD refers Create, Read, Update, Delete

public interface UserRepository extends CrudRepository<User, Long> {

}


CRUD를 이용하여 데이터 제어하기

CURD를 이용한 데이터제어를 유닛테스트기를 이용하여 테스트해보겠습니다.

간단하게 사용자를 추가하고, 조회를 하는 코드입니다.

Use Case
package com.psmon.springdb;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class JparestdemoApplicationTests {
	
	@Autowired
	private UserRepository userRepository;

	@Test
	public void contextLoads() {		
		jpaTest1();		
	}
	
	public void jpaTest1() {
		// 사용자 생성
		User addUser = new User();
		addUser.setName("minsu");
		addUser.setEmail("test@x.com");
		userRepository.save(addUser);
		
		// 사용자 조회
		Iterable<User> userList = userRepository.findAll();		
		userList.forEach(item->System.out.println(item.getName() ));		
	}
}


위코드는 실제 아래와같은 SQL문을 실행합니다. 

-- 사용자생성
INSERT INTO `spring`.`user`
(`id`,
`email`,
`name`)
VALUES
(<{id:}>,
<{email: >,
<{name:}>);

-- 사용자 조회
SELECT * FROM user



JPA Relation

일반적으로 DB의 테이블은 하나의 테이블에 모든 정보를 포함하여 설계하지않고,

데이터의 효율적인 관리를 위해서 몇개의 테이블구조로 나누어 저장후 외래키로 연관관계 형성을하게 됩니다. 

이렇게 구조적으로 나뉜 테이블을 하나의 테이블정보인것처럼 정보를 머지를 하려면 JOIN을 통해 테이블을 지배해야합니다.

JPA 객체처리모델에서는 SQL문의 JOIN문을 직접적으로 사용하지 않고, 동일한 효과를 내려고 합니다.

그 목적을 달성하기위해 데이터베이스의 테이블과 객체지향의 몇가지 차이점을 알아야합니다.

Class VS Table

class User{
	int id;   // 특별한 구현없이, 사용자의 객체만으로 자신을 포함하는 특정정보(클릭로그)만 추출할수가 없습니다.
	string name;
	string email;
}
class ClickLog{
	int clickid;
	User user; // User객체 참조를 포함합니다.
	string clickurl;	
}

TABLE User{
   int id   //자신의 키를 참조하는 다른 테이블에 접근가능합니다. on u.id = c.clickid
   varchar name
   varchar email   
}
TABLE ClickLog{
	int clickid
	int userid  //외래키를 통해서 접근가능합니다. on c.clickid = u.id
	varchar clickurl;
}


  • 프로그래밍모델에서 객체는 일반적으로 상속또는 포함관계를 통해 단방향 접근이 일반적입니다.. ( ClickLog→ User )
  • 데이터 베이스는 연관관계와는 별개로,  양방향 접근에 자유롭습니다. ( User ↔ ClickLog )
  • 프로그래밍 모델에서는 상속의 개념이 존재하여 다양한 객체를 포함할수가있습니다. 데이터베이스 에서는 상속개념이 없으며 리스트와같은 데이터Type을 포함할수 없습니다.
  • 데이터베이스에서는 상속의 개념을, 외래키를 통해 관계형성을 하며 논리적인 관계를 형성할수가 있습니다.


이와같은 차이를 극복하고, 데이터베이스의 Table을 객체지향적 인 모델로 변경을하여 사용하려면

JPA에서 지원하는 관계(Relation)을 형성(Join)하는 몇가지 키워드의 의미를 알아야합니다.

아래 키워드는 OOP가 가진 특성을 잃지않고  OOP가 가진 한계를 관계형 DB처럼 확장을 해줍니다.

  • ManyToOne : 다대일 관계 매칭정보
  • JoinColumn : 외래키를 매핑때 사용함
  • mappedBy : 연관관계설정시 주인이 아님을 설정
  • OneToMany : 일대다 관계 매핑정보
  • OneToOne : 일대일 관계 매칭정보, 어느곳이나 외래키를 가짐
  • ManyToMany : 다대다 관계매칭정보, 맵핑테이블을 만들어서 사용하기를 권장
  • 연관관계주인 : 외래키가 있는곳이 주인이며 주인만이 수정가능 아닌경우 조회만가능


외래키/인덱스

JPA를 사용하면 , 테이블생성을 SQL문으로 직접 하지 않는 전략을 선택하기 때문에

인덱스설정/외래키설정등이  자동적으로 생성됩니다. 자신이 설계한, JPA CLASS가 어떠한 테이블 스키마에서

어떠한 관계가 형성이되고 인덱스를 설정하는지 파악하는것은 중요한 사안입니다.


반대로, 테이블 생성을 개발자가 할수 없는 체계라고 하면, spring.jpa.hibernate.ddl-auto=none 전략으로 갈수도 있습니다.

이경우 운영 DB의 테이블 업데이트는 어플리케이션이 할수 없는 권한구조가 미리 약속되었다라고 해둡시다.

JPA에서는 어쨋든 기존 테이블 스키마를 ,OOP로 옮기려고 할것이고 테이블에 설계된 외래키 설정및 1:? 관계를 파악하지 못하고

JPA를 사용하려고 한다면,  연관된 테이블의 참조를 얻기가 불가능할것입니다.

SQL중심적 개발에서는 자유롭게 JOIN을 하여 모든 테이블 참조를 얻을수도 있고 집합도 만든반면,

JPA에서는 테이블의 연관성을 파악하지 못하면 OOP 레벨에서 아무것도 할수 없는 상황이 생깁니다.

이러한 제약은 오히려 데이터베이스를 더 공부해야하는 요소라고 볼수 있습니다.


식별관계 VS 비식별관계

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 그러면 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비지니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 반면에 비식별관계의 기본 키는 비지니스와 전혀 관계없는 대리 키를 주로 사용한다. 언제든지 요구사항은 변한다. 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.



이후 샘플은 비식별관계만 이용하여, JPA 연관특성을 살펴 보겠습니다.



ManyToOne

위 샘플에서 사용자(User) 데이터 모델링에서,  사용자가 속한 그룹을 표한하는 테이블을 추가해보겠습니다.

여러명의 사용자는 한개의 그룹에 속할수 있기때문에, 사용자의 입장에서 다대일 입니다.

즉 사용자의 모델링에서만.., 다대일 설정을 그룹 정보와 맺어주면됩니다.

-부모의 키와 자식의 키가 다르며(전파되지 않았으며) , 자식의 테이블에서만 부모를 찾는 참조(외래)키만 있기때문에 비식별관계입니다.


SQL MODE

우리가 원하는 테이블 모델링은 위와같을 모습이며, user테이블에 group_id가 외래키로 설정이 되어있습니다.

어플리케이션에서 레거시한 제어를 모두한다고 하면, 이러한 외래키 설정이 필요없을수도 있으나

데이터 무결성의 입장에서 중요한 설정이며, JPA 사용시 외래키를 직접 지정하지는 않지만 

Relation을 사용하면,  외래키설정이 적절한 위치에 자동으로 됩니다. 

자동으로 된다고 이러한 개념을 무시하면 안되고, DB 테이블 설계시 고려되어야하는 일반적인상황을

OOP 작성시 고민해야 한다는 점입니다.


SQL문으로 표현
SQL문으로 표현했을시
CREATE TABLE `group_info` (
  `group_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`group_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


CREATE TABLE `user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `group_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`user_id`),
  KEY `FKa36i4ekojwk70bxen390i6tek` (`group_id`),
  CONSTRAINT `FKa36i4ekojwk70bxen390i6tek` FOREIGN KEY (`group_id`) REFERENCES `group_info` (`group_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


JPA MODE

GroupInfo Entity
@Entity
public class GroupInfo {
	@Id
    @GeneratedValue(strategy=GenerationType.AUTO)
	@Column(name = "GROUP_ID")
    private Integer id; 
    
    private String name;    
}

User Entity
@Entity
public class User {
	@Id
    @GeneratedValue
    @Column(name = "USER_ID")
    private Integer id;

	private String name;

	private String email;
 
	@ManyToOne
	@JoinColumn(name = "GROUP_ID")
    private GroupInfo groupInfo;	
}

사용케이스
@Autowired
private GroupRepository groupRepository;


@Autowired
private UserRepository userRepository;

GroupInfo newGroup = new GroupInfo();
newGroup.setName("학생");
groupRepository.save(newGroup);

// 사용자 생성
User addUser = new User();
addUser.setName("minsu");
addUser.setEmail("test@x.com");
addUser.setGroupInfo(newGroup);        
        
userRepository.save(addUser);
 
// select * from user join group_info 과 동일한 효과로, 분리된 테이블에서 그룹명을 가지고 온다.
Iterable<User> userList = userRepository.findAll();      
userList.forEach(item->System.out.println( String.format("Name:%s  GroupName:%s", item.getName(),item.getGroupInfo().getName() )  ));


전통적인 처리 방법은 Insert쿼리를 실행하고 다시조회쿼리를 실행하고,그 데이터셋의 결과를 어플리케이션에 가지고와서
사용하기 과정까지 각각 다른 처리코드와 변환과정이 필요했을것입니다.( 테이블을 쿼리로 설계 <-> SQL <-> DataSet <-> Object <-> Json )
Json의 뷰단의 데이터가 하나만 바뀌어도 최대 5가지 수정 포인트에서 코드 수정이 이루어 졌을것입니다.

JPA를 통한 데이터모델링 정의및 데이터제어가 다소 익숙하지않고 SQL문의 자유롭고 복잡한
표현을 모두 표현하기에 어려울수도 있습니다. 현재로서는 일괄적인 단일지점( 객체지향 정의)에서
모두 가능하다란것정도 이해를 해두고 넘어갑시다.


OneToMany

테이블구조는 변함이 없으며 접근 방식을 ManytoOne에서 OneToMany로 변경을 하여 객체지향접근방식으로

접근해보겠습니다.  Group을 핸들링하고 User는 리스트처럼 사용하는것을 시도해보겠습니다.

SQL MODE

DB는 구조상, 하나의 테이블에서 서브 List형태의 데이터를 가질수가 없습니다.

각각의 테이블에서 그룹정보 / 사용자정보를  따로 입력해야하며 .. 조회시 Join을 통해

그룹정보가 사용자리스트를 가진것처럼 집합을 만들어야합니다.

-- 그룹을 추가한다.
INSERT INTO test.group_info(
   group_id
  ,name
) VALUES (
   NULL -- group_id - IN int(11)
  ,''  -- name - IN varchar(255)
)


-- 사용자를 추가할시 그룹을 지정하거나, 나중에 지정한다.
INSERT INTO test.user(
   user_id
  ,email
  ,name
  ,group_id
) VALUES (
   NULL -- user_id - IN int(11)
  ,''  -- email - IN varchar(255)
  ,''  -- name - IN varchar(255)
  ,0   -- group_id - IN int(11)
)


-- 여러명을 등록할시 위 과정이 반복됩니다.




--학생인 사용자만 조회시..Join을 통해 해결
select * from user join group_info gi on gi.name='학생'



JPA MODE 

JPA에서의 목적은, 기존 SQL에서 처리하는 방식을 객체지향적으로 변경하는것입니다.

그리고 Join사용에 있어서 기존 테이블의 관계(외래키관계)를 잘 설계해야한다는것입니다.

Join에서 해방되고, 외래키 설정같은것을 신경쓸필요가 없어보이나

JPA를 활용한 코드가, 외래키 설정도 실제 되고, Join SQL문이

FindByname 내에 내재되어 작동이 된다라는 사실입니다.

각종 Join(Inner,Left,Right....)에 대응하는 JPA Relation 키워드기능을  잘 파악해둬야합니다.

SQL Join의 자유로운 표현력에 비해 ,JPA는 분명 제약적입니다. 

객체 지향적 설계가 어떠한 Join문을 실제 작동시키게될지? 성능적으로 어떠한 전략을 택해야할지?

오브젝트 설계시 고려가 되지 못하면, Join 기능을 사용조차 할수없는 답답함이 생길수도 있습니다.

이것을 장점으로 받아들일지? 단점으로 생각해야할지? JPA를 선택하는 중요한 사안이될수 있습니다.


GroupInfo에 1:N 포함관계처리 추가
@Entity
public class GroupInfo {	  
  @OneToMany(mappedBy = "groupInfo", cascade = {CascadeType.PERSIST},fetch=FetchType.EAGER)
  private Set<User> users;

	@Override
    public String toString() {
        String result = String.format(
                "GroupInfo[id=%d, name='%s']%n",
                id, name);        
        if (users != null) {        	        	
            for(User user : users) {
                result += String.format(
                        "User[id=%d, name='%s']%n",
                        user.getId(), user.getName());
            }
        }
        return result;
    }
}


public interface GroupRepository extends CrudRepository<GroupInfo, Long> {
	public GroupInfo findByName(String name);	
}
  • cascade : 속성값에는 CascadeType라는 enum에 정의 되어 있으며 enum값에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH가 있습니다.
  • targetEntity : 관계를 맺을 Entity Class를 정의합니다.
  • fetch : FetchType.EAGER, FetchType.LAZY로 전략을 변경 할 수 있습니다. 두 전략의 차이점은 EAGER인 경우 관계된 Entity의 정보를 미리 읽어오는 것이고 LAZY는 실제로 요청하는 순간 가져오는겁니다.
  • mappedBy : 양방향 관계 설정시 관계의 주체가 되는 쪽에서 정의합니다.
  • orphanRemoval : 관계 Entity에서 변경이 일어난 경우 DB 변경을 같이 할지 결정합니다. cascade와 다른것은 cascade는 JPA 레이어 수준이고 이것은 DB레이어에서 처리합니다. 기본은 false입니다.



Test
GroupInfo newGroupA = new GroupInfo("학생");
Set usersA = new HashSet<User>() {{
	add(new User("minsu2","min2@x.com",newGroupA));			
	add(new User("minsu3","min3@x.com",newGroupA));
}};
newGroupA.setUsers(usersA);

GroupInfo newGroupB = new GroupInfo("선생");
Set usersB = new HashSet<User>() {{
	add(new User("tom1","tom1@x.com",newGroupB));			
	add(new User("tom2","tom2@x.com",newGroupB));
}};
newGroupB.setUsers(usersB);

    groupRepository.save(new HashSet<GroupInfo>() {{
    	add(newGroupA);
    	add(newGroupB);        	
    }});
    

GroupInfo groupInfo = groupRepository.findByName("학생");        
    System.out.println( String.format("학생정보: %s", groupInfo.toString() ) );

위 테스트 코드는, 각각의 그룹을 생성을 하고 사용자까지 Group에 포함하여 저장하는 방식을

사용하였습니다. 전형적인 객체 지향적인 접근 방식이며

최종 save명령을 통해, 실질적인 저장쿼리가 순차적으로 모두 반영을 할것입니다.

또한 마지막에는, 그룹명을 통해 학생리스트를 조회하는 명령을 조인을통한 테이블 집합변경

없이 수행하였습니다. ( 물론 , 실제 이 조회명령을 수행하기위해 Join문이 작동됩니다.)


Paging 처리

JPA MODE

public interface UserPageRepo extends Repository<User, Long>{

	Optional<User> findOne(Long id);
	
	// 전체 페이징처리
	Page<User> findAll(Pageable pageRequest);
	
	// 검색 확장-그룹명으로 페이지 필터
	Page<User> findByGroupInfoName(String groupName,Pageable pageRequest);
		
	// Update
	void delete(User deleted);
	
	User save(User persisted);
	
	void flush();
}

SQL문 쿼리없이, 검색조건/필터조건등을 지정하여 페이징 처리가 가능합니다.

JPA에서는 가변적인 함수처리로, SQL문의 페이징처리가 고려된 복합 검색 연산조합처리 모두가능합니다.

  • JPA함수 : findBy{필드명A}OrfindBy{필드명B}AndfindBy{필드명C}(....인자값);
  • SQL문 : where 필드명A=인자값A or 필드명B=인자값B and 필드명C

더자세한정보는 아래문서를 참고합니다.:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-lookup-strategies


UseCase

		//Test를 위해 100개의 데이터 인입
		GroupInfo newGroupA = new GroupInfo("학생");
		Set usersA = new HashSet<User>() {{
			for(int i=0; i<100 ; i++) {
				String userName = String.format("minsu%d", i);
				String email = String.format("min%d@x.com", i);
				add(new User(userName,email,newGroupA));
			}					
		}};
		newGroupA.setUsers(usersA);
        groupRepository.save(new HashSet<GroupInfo>() {{
        	add(newGroupA);
        }});


		//원하는 페이지를 조회합니다.( 페이지번호 , 페이지당 처리수)
        PageRequest  pageRequest =  new PageRequest(1,10);                       
        Page<User> sPage = userPageRepo.findAll(pageRequest);
        System.out.println( String.format(" %d:Contents %d:Page", sPage.getNumberOfElements(),sPage.getNumber() ) );
        
        Page<User> sPage2 = userPageRepo.findByGroupInfoName("학생", pageRequest);
        System.out.println( String.format(" %d:Contents %d:Page", sPage2.getNumberOfElements(),sPage2.getNumber() ) );


JPQL MODE

public interface UserPageRepo extends Repository<User, Long>{

	// QueryMode
	@Query(value="select t from User t "			
			+ "where  t.name =:name  "
			+ "order by t.id " , nativeQuery=false )
	List<User> findBySomeName( @Param("name") String name, Pageable pageable);
	
}

JPA의 함수처리방식이 익숙하지 않다면, SQL방식으로 인터페이스 작성도 가능합니다.

함수의 인자가, SQL문의 인자로 전달하는 방식의 패턴을 파악한후, 기존 SQL문 활용이 가능합니다.


JPQL VS SQL(Native)

JPQL은 JPA와 연동되어 일반적으로 대부분의 DB에 호환이되는 다소 제약적인 쿼리사용이 가능합니다.

JPQL의 공통된 인터페이스, 예를 들면 데이터를 나뉘는 파티션 인터페이스인 페이징처리를 

각 DB에 맞게끔 모두 처리가가능합니다.

이와 반대로  SQL(Native) 모드로 사용도가능하며, DB변경시 호환이 깨질수 있습니다.

예를 들어 페이징을 위해 Mysql에만 있는 페이징처리를 위해 Limit기능을 사용할수 있지만

DB변경시 관련 SQL문을 지원하지 않는다면, 작동하지 않을것입니다.

nativeQuery , true/flase에따라 작동방식이 변경가능하며 , 특수한 경우가 아니면

JPQL사용이 권장됩니다.



다른 진영(.net) 에서도 JPA와 유사하게 데이터를 제어하고 있습니다.

Entity Framework

https://docs.microsoft.com/en-us/ef/core/




  • No labels