[WPF] Database(SQLite) 사용해보기

[WPF] Database(SQLite) 사용해보기

이번 글에서는 local PC에 Database 파일을 만들고 데이터 저장, 화면 표시를 해보겠습니다. 데이터베이스는 Sqlite를 사용하도록 하겠습니다. 보통 DB 데이터를 select하거나 insert 하려면 SQL문법을 사용하는데, C#에서는 EF Core(Entity Framework Core)라는 ORM(Object-Relational Mapper)을 사용하면 SQL을 쓰지 않고 DB 데이터를 다룰 수 있습니다.

EF Core의 핵심적인 요소는 아래와 같습니다.

구성 요소설명
DbContext데이터베이스 세션을 나타내는 핵심 클래스
DbSet<TEntity>데이터베이스의 특정 테이블과 1:1로 매핑되는 엔터티(Entity) 타입의 집합(Collection)을 나타내는 클래스

Sqlite 설치

우선 NuGet에서 Microsoft.EntityFrameworkCore.Sqlite를 설치해줍니다. 저는 .NET 8.0이므로 버전 8.0.24로 설치하였습니다. 닷넷 버전에 맞춰 설치 해야 합니다.

DbContext

위에서 설명한 것 처럼 Database 세션을 나타내는 DbContext를 상속 받는 클래스 AppDbContext.cs를 만들어 줍니다. 이 클래스에서 db 파일도 생성할 것이며 table도 생성 될 것입니다. 이 클래스에 DbSet<TEntity> 타입을 사용하여 속성을 선언하면 자동으로 TEntity에 맞는 테이블을 생성해줍니다. 이 글에서는 Student로 만들어 보겠습니다. 그리고 OnConfiguring 함수에서 데이터베이스를 구성하기 위해 메서드를 재정의 합니다.

public class AppDbContext : DbContext
{
    #region fields
    private SqliteConnectionStringBuilder? _sqliteBuilder = null; // sqlite
    #endregion

    #region Tables
    public DbSet<Student> Students { get; set; }
    #endregion

    public AppDbContext()
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 데이터베이스를 구성하기 위해 메서드 재정의
        if (_sqliteBuilder != null)
            optionsBuilder.UseSqlite(_sqliteBuilder?.ToString());
    }

    public void ConnectSqlite(string dbFilePath, SqliteOpenMode mode = SqliteOpenMode.ReadWriteCreate)
    {
        SqliteConnectionStringBuilder sqliteBuilder = new SqliteConnectionStringBuilder();
        sqliteBuilder.DataSource = dbFilePath;
        sqliteBuilder.Mode = mode;
        _sqliteBuilder = sqliteBuilder;

        Database.EnsureCreated();
        Students.Load();
    }

    public void CloseSqlite()
    {
        var connection = Database.GetDbConnection() as SqliteConnection;
        if (connection != null)
            SqliteConnection.ClearPool(connection);
    }
}

그리고 ConnectSqlite와 CloseSqlite 함수를 통해 데이터베이스를 연결하고 종료합니다.

이 다음은, 위에서 나온 Student.cs를 아래와 같이 만들어 줍니다. 아이디, 이름, 학년 정보를 담고 있습니다.

public class Student
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; } = "";
    public int Grade { get; set; } = 0;
}

여기까지 만들었으면 데이터베이스 코드는 대략적으로 완성 되었습니다.

UI

MainWindowView.xaml에서는 아래와 같이 만들어줍니다.

<Window x:Class="WpfDatabase.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfDatabase"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="1000">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <!--#region status-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0"
                       Margin="5"
                       TextAlignment="Right"
                       Text="Status: "/>
            <TextBlock Grid.Column="1"
                       Margin="5"
                       Text="{Binding Status}"/>
        </Grid>
        <!--  connection database  -->
        <Grid Grid.Row="1">

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="100"/>
                <ColumnDefinition Width="100"/>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0"
                       Margin="5"
                       TextAlignment="Right"
                       Text="Db File Path: "/>
            <TextBox Grid.Column="1"
                     Margin="5"
                     Text="{Binding DbFilePath}"/>
            <Button Grid.Column="2"
                    Margin="5"
                    Content="Connection"
                    Command="{Binding ConnectDbFilePathCommand}"/>
            <Button Grid.Column="3"
                    Margin="5"
                    Content="Close"
                    Command="{Binding CloseDbFilePathCommand}"/>
            <Button Grid.Column="4"
                    Margin="5"
                    Content="Save"
                    Command="{Binding SaveStudentCommand}"/>
        </Grid>
        <!--    -->

        <GroupBox Grid.Row="2"
                  Margin="5"
                  Header="Students">
            <DataGrid Margin="5"
                      ItemsSource="{Binding Students}"
                      AutoGenerateColumns="False"
                      IsReadOnly="False"
                      CanUserAddRows="True"
                      SelectionUnit="FullRow"
                      HeadersVisibility="Column"
                      VirtualizingStackPanel.ScrollUnit="Pixel"
                      VirtualizingStackPanel.VirtualizationMode="Recycling"
                      ScrollViewer.CanContentScroll="True">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Id"
                                        Width="Auto"
                                        IsReadOnly="True"
                                        Binding="{Binding Id}"/>
                    <DataGridTextColumn Header="Name"
                                        Width="1*"
                                        Binding="{Binding Name}"/>
                    <DataGridTextColumn Header="Grade"
                                        Width="1*"
                                        Binding="{Binding Grade}"/>
                </DataGrid.Columns>
            </DataGrid>
        </GroupBox>
    </Grid>
</Window>

.db 파일 경로를 입력하고 Connection 버튼을 클릭하면 연결, Close 버튼을 클릭하면 연결해제, Save버튼을 누르면 수정된 사항 .db파일에 저장하는 기능을 가지고 있습니다.

MainViewModel.cs에는 아래와 같이 만들어줍니다.

public class MainViewModel : BindableBase
{
    #region fields, properties
    private AppDbContext? databaseContext = null;
    public AppDbContext? DatabaseContext { get => databaseContext; set => SetProperty(ref databaseContext, value); }

    private string status = "";
    public string Status { get => status; set => SetProperty(ref status, value); }

    private string dbFilePath = "";
    public string DbFilePath { get => dbFilePath; set => SetProperty(ref dbFilePath, value); }

    private ObservableCollection<Student>? students = null;
    public ObservableCollection<Student>? Students { get => students; set => SetProperty(ref students, value); }
    #endregion

    #region commands

    public DelegateCommand ConnectDbFilePathCommand { get; private set; }
    public DelegateCommand CloseDbFilePathCommand { get; private set; }
    public DelegateCommand SaveStudentCommand { get; private set; }

    private void OnConnectDbFilePath()
    {
        try
        {
            DatabaseContext = new AppDbContext();
            DatabaseContext.ConnectSqlite(DbFilePath);

            Students = DatabaseContext.Students.Local.ToObservableCollection();
            Status = "Connect Database";
        }
        catch (Exception e)
        {
            Status = "Connect Database Fail";
        }
        finally
        {
        }
    }

    private bool CanConnectDbFilePath()
        {
            if (DatabaseContext != null)
                return false;
            if (DatabaseContext == null)
            {
                if (string.IsNullOrEmpty(DbFilePath))
                    return false;
            }

            return true;
        }


    private void OnCloseDbFilePath()
        {
            DatabaseContext?.CloseSqlite();
            DatabaseContext?.Dispose();
            DatabaseContext = null;
            Status = "Close Database";

            Students = null;
        }

    private bool CanCloseDbFilePath()
        {
            if (DatabaseContext == null)
                return false;

            return true;
        }

    private void OnSaveStudent()
    {
        DatabaseContext?.SaveChanges();

        Status = "Save";
    }

    private bool CanSaveStudent()
    {
        if (DatabaseContext == null)
            return false;
        return true;
    }
    #endregion

    #region constructor
    public MainViewModel()
    {
        DbFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "School.db");

        ConnectDbFilePathCommand = new DelegateCommand(OnConnectDbFilePath, CanConnectDbFilePath).ObservesProperty(() => DatabaseContext); ;
        CloseDbFilePathCommand = new DelegateCommand(OnCloseDbFilePath, CanCloseDbFilePath).ObservesProperty(() => DatabaseContext); ;
        SaveStudentCommand = new DelegateCommand(OnSaveStudent, CanSaveStudent).ObservesProperty(() => DatabaseContext);
    }
    #endregion
}

실행하여 동작을 확인해봅니다.

실제로 보면 해당 경로에 아래와 같이 School.db가 생성되어 있습니다.

그리고 HeidiSQL로 열어보면 Students 테이블이 생성되어 있고, 칼럼도 만들어져 있으며, 입력한 내용이 들어있습니다.

테이블 특성

Student 클래스에서 몇 가지 속성을 넣으면 테이블 생성 시 더욱 기능이 풍부하게 만들 수 있습니다. 아래는 자주 사용되는 특성들 입니다.

테이블 및 칼럼 매핑에 사용되는 특성들 입니다.

특성설명
[Table(“이름”)]클래스를 특정 이름의 테이블로 매핑
[Column(“이름”)]속성을 특정 이름의 컬럼으로 매핑
[NotMapped]DB 테이블 생성에서 제외

기본 키 및 값 생성에 관련된 특성입니다.

특성설명
[Key]해당 속성을 Primary Key로 설정
[DatabaseGenerated]값이 생성되는 방식 설정

제약 조건 및 검증에 사용되는 특성입니다.

특성설명
[Required]필수 값 설정 (NOT NULL)
[MaxLength(n)]문자열/배열의 최대 길이 제한
[StringLength(n)]MaxLength와 비슷하지만 최소 길이도 지정 가능
[Range(min, max)]숫자 데이터의 범위를 제한

관계 및 인덱스와 관련된 특성입니다.

[ForeignKey(“이름”)]외래 키 관계 설정
[Index]인덱스 생성 (검색 성능 향상)
[Timestamp]동시성 제어용 버전 관리

위 속성을 몇 개 사용해서 Student.cs를 다시 만들어 보면 아래와 같습니다.

[Table("TBL_STUDENT")]
public class Student
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    [Column("col_id")]
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Column("col_name")]
    public string Name { get; set; } = "";

    [Column("col_grade")]
    public int Grade { get; set; } = 0;

    [NotMapped] // 테이블에 칼럼 생기지 않음 안생김
    public string NotMapped { get; set; } = "";
}

기존 School.db를 삭제하여 다시 프로그램을 실행하면 아래와 같이 테이블이 만들어진 것을 볼 수 있습니다.

이상으로 Sqlite를 사용해보았습니다.


github