
니콜라스의 채널에서 'DB고민 끝내드림' 이라는 영상을 보았다.
SQLite와 PostgreSQL에 대해 호기심이 생기게 되었고 먼저 스타트업에서 MySQL과 함께 많이 사용하는 SQLite에 대해 알아보려 한다.
많은 기업, 사람들이 사용하는 많큼 문서화, 자료들이 많고 대부분의 프로그래밍 언어에서 지원한다.
읽기 전용 명령을 관리하는 데 선호된다.
대부분의 기능들을 지원하고, 초보자들이 사용하기에 좋다.
가장 유명한 database이기에 다음 DB로 빠르게 넘어가겠다!
MySQL보다 좀 더 전문적인 데이터베이스로, 더 많은 기능들이 존재한다
특징으로는 이러한 것들이 있다.
가장 중요한 부분은 이것이다
🔎확장자를 지원한다
PostgreSQL에서는 extension이라는 기능을 제공한다. extension으로 외부 프로그램을 PostgreSQL에 연동하여 추가적인 기능들을 사용할 수 있다.
이를 통해 NoSQL, DB단위 테스트, JwtToken 생성 등등 훨씬 많은 기능들을 사용할 수 있다.
많은 사람들이 잘 못 생각하고 있는 것이 있다. SQ'Lite'라는 이름을 보고 성능이 좋지 않고, 토이 프로젝트에나 사용하는 데이터베이스라 생각할 수 있는데 그것은 옳지 않다.
SQLite는 작다, 빠르다, 믿을 수 있다 는 의미이다.
특징으로는 이러한 것들이 있다.
적은 데이터 타입
NULL, INTEGER, REAL, TEXT, BLOB 이 5개의 유형만 존재하기에 편리하다.
SQLite는 하나의 데이터베이스를 하나의 파일로 관리한다. (임베디드 데이터베이스)
SQLite는 그저 하나의 디스크 파일일 뿐이기에 데이터베이스를 복사하여 붙여넣거나, 이메일로 DB를 전송하거나, USB에 넣을 수 있으며 데이터베이스를 백업하고 복원하는 것이 간단하다.
DB를 유지 관리 해줄 필요가 없다.
애플리케이션 코드가 있는 서버에 데이터 베이스가 있기에
dependencies 추가
implementation 'org.xerial:sqlite-jdbc:3.46.1.0'
SQLite는 hibernate에서 지원해주지 않아서 직접 SQLDialect를 생성해줘야한다. 😢
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.dialect.function.VarArgsSQLFunction;
import org.hibernate.type.StringType;
import java.sql.Types;
public class SQLDialect extends Dialect {
public SQLDialect() {
registerColumnType(Types.BIT, "integer");
registerColumnType(Types.TINYINT, "tinyint");
registerColumnType(Types.SMALLINT, "smallint");
registerColumnType(Types.INTEGER, "integer");
registerColumnType(Types.BIGINT, "bigint");
registerColumnType(Types.FLOAT, "float");
registerColumnType(Types.REAL, "real");
registerColumnType(Types.DOUBLE, "double");
registerColumnType(Types.NUMERIC, "numeric");
registerColumnType(Types.DECIMAL, "decimal");
registerColumnType(Types.CHAR, "char");
registerColumnType(Types.VARCHAR, "varchar");
registerColumnType(Types.LONGVARCHAR, "longvarchar");
registerColumnType(Types.DATE, "date");
registerColumnType(Types.TIME, "time");
registerColumnType(Types.TIMESTAMP, "timestamp");
registerColumnType(Types.BINARY, "blob");
registerColumnType(Types.VARBINARY, "blob");
registerColumnType(Types.LONGVARBINARY, "blob");
// registerColumnType(Types.NULL, "null");
registerColumnType(Types.BLOB, "blob");
registerColumnType(Types.CLOB, "clob");
registerColumnType(Types.BOOLEAN, "integer");
registerFunction("concat", new VarArgsSQLFunction(StringType.INSTANCE, "", "||", ""));
registerFunction("mod", new SQLFunctionTemplate(StringType.INSTANCE, "?1 % ?2"));
registerFunction("substr", new StandardSQLFunction("substr", StringType.INSTANCE));
registerFunction("substring", new StandardSQLFunction("substr", StringType.INSTANCE));
}
public boolean supportsIdentityColumns() {
return true;
}
public boolean hasDataTypeInIdentityColumn() {
return false; // As specify in NHibernate dialect
}
public String getIdentityColumnString() {
// return "integer primary key autoincrement";
return "integer";
}
public String getIdentitySelectString() {
return "select last_insert_rowid()";
}
public boolean supportsLimit() {
return true;
}
protected String getLimitString(String query, boolean hasOffset) {
return new StringBuffer(query.length() + 20).append(query).append(hasOffset ? " limit ? offset ?" : " limit ?")
.toString();
}
public boolean supportsTemporaryTables() {
return true;
}
public String getCreateTemporaryTableString() {
return "create temporary table if not exists";
}
public boolean dropTemporaryTableAfterUse() {
return false;
}
public boolean supportsCurrentTimestampSelection() {
return true;
}
public boolean isCurrentTimestampSelectStringCallable() {
return false;
}
public String getCurrentTimestampSelectString() {
return "select current_timestamp";
}
public boolean supportsUnionAll() {
return true;
}
public boolean hasAlterTable() {
return false; // As specify in NHibernate dialect
}
public boolean dropConstraints() {
return false;
}
public String getAddColumnString() {
return "add column";
}
public String getForUpdateString() {
return "";
}
public boolean supportsOuterJoinForUpdate() {
return false;
}
public String getDropForeignKeyString() {
throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect");
}
public String getAddForeignKeyConstraintString(String constraintName, String[] foreignKey, String referencedTable,
String[] primaryKey, boolean referencesPrimaryKey) {
throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect");
}
public String getAddPrimaryKeyConstraintString(String constraintName) {
throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect");
}
public boolean supportsIfExistsBeforeTableName() {
return true;
}
public boolean supportsCascadeDelete() {
return false;
}
}
spring:
datasource:
url: jdbc:sqlite:practice.db
username: root
password: 1234
driver-class-name: org.sqlite.JDBC
jpa:
hibernate:
ddl-auto: create
database-platform: JavaProject.Sqlite.global.config.SQLDialect
이제 자신있게 실행!.. 하면 안된다..
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: org.hibernate.dialect.identity.IdentityColumnSupportImpl does not support identity key generation
문제가 무엇인지 보았는데 Sqlite는 Mysql과 Autoincrement가 달라서 INDENTITY에서 다른 전략으로 변경해줘야한다.

@Service
@RequiredArgsConstructor
@Transactional
public class SignupService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public TokenResponse execute(SignupRequest signupRequest) {
if (userRepository.existsByAccountId(signupRequest.getAccountId())) {
throw UserAlreadyExistException.EXCEPTION;
}
String password = passwordEncoder.encode(signupRequest.getPassword());
userRepository.save(
User.builder()
.accountId(signupRequest.getAccountId())
.email(signupRequest.getEmail())
.password(password)
.name(signupRequest.getName())
.role(Role.STUDENT)
.build()
);
return jwtTokenProvider.createToken(signupRequest.getAccountId());
}
}
이런식으로 코드를 작성해줬다.
이제 postman을 실행 시키면??
또 에러가 뜬다..
[SQLITE_BUSY] The database file is locked (database is locked)
찾아보니 아이디를 검증하는 메서드 문제였다. SQLite는 원자성을 지키기 위해, 데이터베이스에 write작업이 발생할 때 File단위로 lock을 걸어서, lock을 걸린 상태에서는 현재 데이터베이스에 write/read 작업이 불가능 하다고 한다.
그렇다면 당연히 아이디를 검증하는 메서드를 삭제해주면 잘 작동은 하겠지만,
그렇다고 아이디 검증을 지울 수는 없는 일이다.
그 대신, 서비스 로직을 하나로 commit하지 않게 @Transactional을 제거해준다면 아래와 같이 DB에 스레드가 왔다갔다하며 잘 실행이 된다.
SQLite의 저장방식이 흥미로웠다.
MySQL과 많이 달라서 각각의 특징을 공부하며 코드를 작성하는게 재밌어서 기분 좋게 글을 작성 한 것 같다.
https://stackoverflow.com/questions/16113182/jpa-sqlite-no-such-table-sequence/16306718#16306718
https://www.reddit.com/r/programming/comments/vniiaw/sqlite_or_postgresql_its_complicated/
https://www.youtube.com/watch?v=ocZid4g4UpY
https://deep-jin.tistory.com/entry/Spring-Boot%EC%99%80-sqlite3-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-Hibernate
https://makedotworld.tistory.com/55#google_vignette