sqlite Model Class 설계 문의 ..!

image

안녕하세요. DB관련 문의가 있어 질문 글 올립니다 !

Sqlite, EntityFramework, CodeFirst 를 사용하고 있습니다
Model 구조를 만들어야 할 때 이론적으로 잘 이해가 가질 않아 문의 드려요

  1. ParentModel 생성시에 Id를 기준으로 ChildA, ChildB가 같이 이어지게 하는 방법? 이해 ? 이론적으로 잘 이해를 못하고 있습니다.

  2. 만약에 ParentModel 의 ChildA안의 List형식의 자식이 또 존재 한다면 ? Id를 기준으로 어떻게 이어주는것 인지 …

가르침 부탁드려용

3개의 좋아요

질문이 잘 이해가 되지를 않습니다. 먼저 의도하는 결과를 좀 더 설명해주시면 좋겠습니다.
아래 내용은 제가 질문을 완전히 이해하지 못한 상태에서 답변을 드리는 것이니 참고만 해주시고, 가능하시면 좀 더 의도(결과) 관점에서 부연 설명을 해주시면 좋겠습니다.

image

위의 ERD는 예시입니다.

EF에서는 탐색 속성이 표현되면 관계를 위의 ERD처럼 맺어 주는데요,

ChildAs, ChildBs는 컬렉션 탐색 속성, ParentDataModel은 참조 탐색 속성

ParentDataModel는 주 엔터티가 되고 ChildA, ChildB, ChildAa는 종속 엔터티가 됩니다.

별다른 키를 특성으로 표현하지 않았다면 Id나 [Class이름]Id의 이름으로 키 속성을 표현할 수 있습니다. 그리고 탐색 속성에 의해 엔터티 간의 관계가 형성됩니다.

ChildA와 ChildB가 같이 이어지게 표현하는것을 의미하나요? 그렇다면 아래와 같이 탐색 속성으로 표현할 수 있습니다.

public class ChildA : IEntity
{
    ....
    public virtual ChildB ChildB { get; set; }
}

public class ChildB : IEntity
{
    ...
    public virtual ChildA ChildA { get; set; }
}

그리고 탐색 속성을 통해 결과를 확인할 수 있습니다.

var info = context.ParentDataModels.Where(x => x.Id = 10);
foreach (var childA in info.ChildAs) { ... }
foreach (var childB in info.ChildBs) { ... }

탐색 속성에 의해서 관계를 따라갈 수 있으므로 이렇게 질의도 가능합니다.

var result = context.ParentDataModels.Where(x => x.Id = 10).ChildAs
    .First()
    .ChildB;

List형식의 자식이 제 관점에서는 맞지 않는 표현 같습니다. 자식이 아니라 엔터티 관계상의 주/종 관계일 뿐입니다. 예를들어 사용자정보가 있고 사용자로그인이력이 있다면 사용자정보는 주 엔터티이고 사용자로그인이력은 종 엔터티 입니다. 이것을 탐색 속성으로 표현할 수 있겠고요, 사용자로그인이력은 사용자정보 관점에서 일대다이므로 목록으로 표현됩니다.

이렇게 관계가 형성되었다는 의미는 관계가 형석되었다는 것이고 그 관계는 ID로 식별하므로… 주 엔터티 ID로 이어지게 됩니다.

3개의 좋아요

부연해서 위의 코드는 테스트하지 않는 코드로 참고만 하셨으면 좋겠습니다. 엔터티의 탐색 속성으로 관계를 어떻게 표현할 수 있는가를 중점으로 살펴보시면 좀 더 빠르게 이해하실 수 있지 않을까 생각해봅니다.

2개의 좋아요

efcore는 엔티티의 상태를 보고 추가할지 업데이트할지를 판단합니다.
Parent를 새로 생성하셨고, Child의 Collection도 새로 생성하셨으면 둘다 insert 가 발생 됩니다.
Query를 통해 조회된 내용을 바탕으로 Child의 특정 index의 속성을 변경하셨으면 업데이트가 발생 되실 것 같아요.

그나저나 모델에 기본키 특성이 없어서 Id 값에 제대로 매핑이 안될 것 같은데 DbContext 쪽에 선언하셨나 모르겠네요.

3개의 좋아요

image

제가 원하던 그림을 최대한 이미지로 표시해봤습니다,

아직도 머리에 딱 하고 이해가 되질 않네요,

  1. 고객, 주문, 상세 주문 이 있습니다.
  2. 개별 목표는 이렇습니다.
    2-1. 고객은 여러개의 주문을 가지고 있다.
    2-2. 여러개의 주문 에는 여러개의 상세주문을 가지고 있습니다.
  3. 이와 같음으로, 고객 Class 안에 List Orders를 가지고 Order class 안에 List OrderDetails 를 넣고싶은데요,
    id를 이어주는 방법(?)을 잘 모르겠습니다
3개의 좋아요

제가 외부에 있어서 저녁 늦게 관련된 샘플을 만들어 공유해볼께요

1개의 좋아요

안녕하세요?
Customer : Orders = 1 : n
Orders : OrderDetails = 1 : n 을 원하시는 것으로 이해했는데요,

여기에 보면 일대 다 관계에 대한 Entity Class 정의 방법의 예시를 보여주는데요,
이 부분을 참고해서 일대 다 관계를 중첩해서 구성해보시는 것도 나쁘지 않아 보입니다.

Annotation / Attribute 를 활용한 구성도 있지만
좀 복잡한 구성을 원하신다면 FluentAPI를 쓰셔야 할 것 같아요.

그리고 보통 Parent의 ID만 참조하고
GrandParent의 ID는 참조 안 하지 않나요?;;

4개의 좋아요

그렇군요, GrandParent의 ID는 참조를 잘 안하는지 몰랐었습니다.
혹여나 id를 기준으로 개별로 찾아야할 경우를 생각해서 원했던 것입니다.
관련 내용보고 한번 따라해봐보도록 하겠습니다,
감사합니다 -.

3개의 좋아요

아예 틀린 아이디어는 아니기도 합니다.
SQL을 기준으로 생각하면 대개 join으로 해결하는 경우가 많을 뿐이에요.

쉽게 쓰기 위해서 GrandParentID , ParentID를 둘 다 병기 하는 경우가 있기도 하겠네요
이런 경우엔 더 이상은 의미로는 조부모ID가 아니게 되겠지요.

사용 할 때 어떤 방식으로 구성하는 것이 더 효율적인지는 설계도 해보기도 하고, 프로파일링도 하고 그럽니다. 제 얕은 실무 경험에서도요 ^^;

2개의 좋아요
  1. ParentModel 생성시에 Id를 기준으로 ChildA, ChildB가 같이 이어지게 하는 방법? 이해 ? 이론적으로 잘 이해를 못하고 있습니다.
  • @suwoo 님이 말씀해주신 것처럼 Id 속성에 Key 특성을 적용하시거나 DbContext 상의 FluentAPI로 해결 해야 될 것 같아요.
  • Id가 자동 생성 되길 바라신다면 [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 특성을 적용하시거나 DbContext 상에서 ValueGeneratedOnAdd 함수로 매핑해줘야 되겠네요.
  1. 만약에 ParentModel 의 ChildA안의 List형식의 자식이 또 존재 한다면 ? Id를 기준으로 어떻게 이어주는것 인지 …
  • 글 내용 중에 GrandParent 에 대한 언급이 없어서 어떤 의미인지 잘 이해하지는 못했는데요. ㅠㅠ 궁금하신 부분이 시점상 쿼리할때를 의미 하는 것인지 아니면 추가나 업데이트 하는 시점을 말씀하시는 것인지에 따라 다를 것 같아요.
  • 일반적으로는 GrandParent Id를 이용하는 경우는 많지 않죠~
4개의 좋아요

아, Doc의 링크를 덧붙이려고 덧글을 쓰다보니 이렇게 되어버렸네요 :pray:
의도만 놓고 보자면 울님이 적어주신 댓글에 덧붙이려다 이런게 된 거구요 ㅠㅠ

hask님이 밑에 diagram을 덧붙여 그려주신걸 보니
1:n이 두번 중첩된 것으로 보여서 그렇게 내용이 선회된 것입니다ㅎㅎ

그 외엔, 제가 말씀드리고 싶은부분과 거의 일치하는 것 같네요.

3개의 좋아요

처음 글에서 세 가지 Child의 ID가 전부 똑같이 Id가 되어버려서 더욱 헷갈리게 된 것 같습니다.
OrderDetails Entity를 기준으로 본다면
만약 둘 다 조회가 필요하시다고 했으니,

ID
CustomerID
OrdersID
세개가 들어가게 되겠지요.
그렇다고 해서 Customer Entity가 ICollection<OrderDetails> 를 갖는다는 보장은 할 수 없겠네요.

2개의 좋아요
public class Entity
{
    [Key]
    [Column(Order = 0)]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get;set; }
}

public class Customer : Entity
{
    public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}

public class Order : Entity
{
    [Required]
    public int CustomerId { get; set; }

    [Required]
    [ForeignKey(nameof(CustomerId))]
    public Customer Customer { get; set; }

    public virtual ICollection<OrderDetail> Details { get; set; } = new List<OrderDetail>();
}

public class OrderDetail : Entity
{
    [Required]
    public int OrderId { get; set; }

    [Required]
    [ForeignKey(nameof(OrderId))]
    public Order Order { get; set; }
}

이런 구조일까요?

4개의 좋아요

아 네네 맞는것 같습니다,
헌데 TEST 해보았는데 관계구성이 안되고 있는데 제가 빼먹은 부분이 있는걸까요 ?
Code와 동일하게 구성하여 DB생성까지 해보았습니다.

2개의 좋아요
// See https://aka.ms/new-console-template for more information

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using Microsoft.EntityFrameworkCore;

await using var context = new CustomerContext();
var customer = new Customer { Name = "hoya" };
var product = new Product { Customer = customer, Name = "p1" };
var jobDetail = new JobDetail { Product = product, Name = "j1"};

product.JobDetails.Add(jobDetail);
customer.Products.Add(product);
context.Add(customer);
await context.SaveChangesAsync().ConfigureAwait(false);


public class Entity
{
    [Key]
    [Column(Order = 0)]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get;set; }
}

public class Customer : Entity
{
    [Required]
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; } = new List<Product>();
}

public class Product : Entity
{
    [Required]
    public int CustomerId { get; set; }

    [Required]
    [ForeignKey(nameof(CustomerId))]
    public Customer Customer { get; set; }

    [Required]
    public string Name { get; set; }

    public virtual ICollection<JobDetail> JobDetails { get; set; } = new List<JobDetail>();
}

public class JobDetail : Entity
{
    [Required]
    public int ProductId { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    [ForeignKey(nameof(ProductId))]
    public Product Product { get; set; }
}

public class CustomerContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<JobDetail> JobDetails { get; set; }

    public string DbPath { get; }

    public CustomerContext()
    {
        var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        DbPath = System.IO.Path.Join(path, "customer.db");
    }

    // The following configures EF to create a Sqlite database file in the
    // special "local" folder for your platform.
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite($"Data Source={DbPath}");
}

잘 되는거 같은데 어떤 점이 다를까요?
sqlite 상에서 foreign key도 잡혀 있네요.
추가 하실 때 parent가 되는 Customer나 Product에 인스턴스가 설정 되어 있는지 확인해 보시는 것도 좋을 것 같네요.

image

5개의 좋아요

아 네네네 ! 아주 잘되고 있습니다.
instance 설정이안되어 있어서 안들어갔었습니다 !! ㅎㅎ
감사합니다.

추가적으로 궁금한것이 있는데
ModelConfiguration 에서 관계를 형성 짓는 방법이 따로 없는듯 해서
막연하게 따라하긴 했지만 조금 더 세부적으로 다시 한번 알려주실 수 있을까요 …?
특히 이해안가는 부분이 이전에 1:N 방식을 했을때는
[Key]
[Column(Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[ForeignKey(nameof(ProductId))]
요런 문구를 사용하지 않고 사용했었습니다.
제 눈치로는 저 attribute 가 관계 형성을 이어주는것 같은데 아닐까요…?

마지막으로 감사합니다 !! 잘됩니다 ! ㅎㅎ

5개의 좋아요

좋은 애티튜드에 더 많이 알려드리고 싶은 마음이 뿜뿜하네요. :slightly_smiling_face:

각 특성의 의미는 아래와 같습니다.

  1. Column 특성 및 Order 속성
    컬럼 생성 순서를 지정합니다.

  2. Key 특성 + DatabaseGenerated 특성

  • Key 특성은 기본키를 의미합니다.
  • DatabaseGenerated는 Row 처리 방식을 지정합니다.
  • int형 속성에 Key 특성을 주고 DatabaseGeneratedOption.Identity를 적용했으니 AUTO INCREMENT가 된다고 보시면 됩니다.
  • 그 외에도 DateTime형 속성에 줄수도 있고, DatabaseGeneratedOption.Computed 값으로 다른 형태의 시나리오도 적용하실 수 있습니다. 댓글로 모두 알려드리기는 어려워 관련 시나리오 발생시 관련 문서를 찾아 보시면 좋을 것 같네요.
  1. ForeignKey 특성
  • 이 특성이 1:N을 만드는 특성입니다.
  • Parent 클래스에 Child 클래스의 ICollection 속성을 선언하고, Child 클래스에 Parent 속성을 선언하시면 기본 동작합니다.
    그래서 예제 코드를 보시면, Customer 클래스에 ICollection Products 속성이 있고, Product 클래스에는 Customer 속성이 선언되어 있습니다.
public class Customer : Entity
{
    [Required]
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; } = new List<Product>();
}

public class Product : Entity
{
    [Required]
    public int CustomerId { get; set; }

    [Required]
    [ForeignKey(nameof(CustomerId))]
    public Customer Customer { get; set; }

    [Required]
    public string Name { get; set; }

    public virtual ICollection<JobDetail> JobDetails { get; set; } = new List<JobDetail>();
}

결론은 1,2번은 관계 설정과 연관이 없고, 3번이 관계 설정과 연관이 있습니다.
아 그리고 특성을 선언하는 것 외에 @suwoo 님이 언급하신 것처럼 ModelConfiguration 에서 FluentAPI로 관계를 설정하실 수 있습니다.

builder.HasMany(c => c.Comments)
   .WithOne(c => c.Ticket)
   .OnDelete(DeleteBehavior.Cascade); // cascade

builder
   .HasOne(c => c.User)
   .WithMany(c => c.Notices)
   .HasForeignKey(x => x.UserId)
   .OnDelete(DeleteBehavior.Cascade); // cascade

이런식으로요.

5개의 좋아요

네네 ! ForeignKey에 대해 이제 조금 알 것 같습니다!! 세세히 설명해 주셔서 감사합니다 !ㅎㅎ

Key특성, DatabaseGenerated 을 찾아서 다시한번 학습해보도록 하겠습니다

추가적으로, (계속생기게 되네요, 허허 )
생성시킬 때는 정상적으로 생성이 잘 되고있습니다.
헌데 삭제 [Products 와 JobDetails의 요소중 한개] 시키려고 하니
DbSet.FInd(item) 행위를 하니 찾지를 못하더라구요
이부분에서 실수한게 있을까요?

3개의 좋아요

image
Find의 파라미터 설명을 보시면 key 값 전달이 필요한 걸 알수 있는데요.
스샷처럼 특정 도메인 DbSet의 Id를 넘겨주셔야 찾아질겁니다.
Products DbSet이면, Product의 Id니까 1,2,3와 같은 숫자가 되겠네요.

3개의 좋아요

image

image

요렇게 하고있습니다.

image
Find 미실행 하고 Remove만 할시에는 정상동작 합니다…!

2개의 좋아요