Goã®ORMãSQLBoilerã®ã¹ã¹ã¡
ãã®è¨äºã¯ suusan2go Advent Calendar 2019 - Adventar ã®4æ¥ç®ã®è¨äºã§ãã
ããªã¼ã©ã³ã¹å§ãã¦ãããã¨ã ã¹ãªã¼ã§ãä¸è©±ã«ãªã£ã @maeharin ãããCTOããã¦ã ANNONE ã¨ããä¼ç¤¾ããæä¼ããã¦ãã¾ããèªåã¯ãã®ä¸ã§ãGoã«ããã¢ããªåãã®APIï¼Reactã«ãã管çç»é¢ãFlutterã¢ããªãCloudSQL=> BQã®åæãæ°è¦äºæ¥ã®Rails newãªã©ã¨å¹ åºãè²ã ã¨ãããã¦ããã£ã¦ããã®ã§ãããä»åã¯ãã®ã ã¨ããã¢ããªåãã«ä½ãããGoã®APIã®DBã¢ã¯ã»ã¹ãSQLBoilerã«åãæ¿ãã話ããã¾ãã
Go APIã®æ§æ
ç¹å®ã®ãã¬ã¼ã ã¯ã¼ã¯ã¯ä½¿ã£ã¦ãããã«ã¼ãã£ã³ã°ã«ã¯ gorilla/muxãDBã¢ã¯ã»ã¹ã«ã¯ sqlxã使ã£ã¦ãã¦ãã·ã³ãã«ãªã¬ã¤ã¤ã¼ãã¢ã¼ããã¯ãã£ãæ¡ç¨ãã以ä¸ã®ãããªæ§æã«ãªã£ã¦ãã¾ãã
api âââ application âââ cmd âââ controller âââ domain âââ middleware âââ migration âââ repository âââ util âââ view
DBã¢ã¯ã»ã¹ã¯repositoryã«è¨è¼ãããinterfaceãå®è£ ããå½¢ã§å®ç¾ããã¦ãã¾ãã
type Repository interface { FindClinicByID(q sqlx.Queryer, clinicID int64) (*domain.Clinic, error) CreateClinic(tx *sqlx.Tx, clinic *domain.Clinic) (int64, error) }
// FindClinicByID IDã§ã¯ãªããã¯ãæ¤ç´¢ãã func (r *repository) FindClinicByID(q sqlx.Queryer, clinicID int64) (*domain.Clinic, error) { c := domain.Clinic{} query := ` select c.* from clinics as c where c.id = $1 ` if err := sqlx.Get(q, cd, query, clinicID); err != nil { if err == sql.ErrNoRows { return nil, errors.WithStack(NewRecordNotFoundError(fmt.Sprintf("clinic(id: %d) is not found", clinicID))) } return nil, errors.WithStack(err) } return cd, nil } // CreateClinic ã¯ãªããã¯ãç»é²ãã func (r *repository) CreateClinic(tx *sqlx.Tx, diary *domain.Clinic) (int64, error) { query := ` insert into clinics( , name , create_timestamp , update_timestamp ) values ( , :name , :create_timestamp , :update_timestamp ) returning id ` stmt, err := tx.PrepareNamed(query) defer stmt.Close() if err != nil { return int64(0), errors.WithStack(err) } var id int64 err = stmt.Get(&id, &diary) if err != nil { return int64(0), errors.WithStack(err) } return id, nil }
sqlxã«ããDBã¢ã¯ã»ã¹ã®pros / cons
Goã§ã¯ããã¾ããããéå»ã®ããã¸ã§ã¯ãã§Domaãªã©SQLãæ¸ãã¦ããããªãã¸ã§ã¯ãã«ãããã³ã°ããå½¢å¼ã®ã©ã¤ãã©ãªã¯ä½¿ã£ããã¨ããããSQLãæ¸ãã¦ãªãã¸ã§ã¯ãã«ãããã³ã°ããã¨ããææ³ã¯ã·ã³ãã«ã§æ°ã«å ¥ã£ã¦ãã¾ããç¹ã«è¤æ°ã®ãã¼ãã«ãã¸ã§ã¤ã³ãã¦ãªãã¸ã§ã¯ãã«ãããã³ã°ãããã¨ãã£ããã¨ãã·ã³ãã«ã«å®ç¾ã§ãã¾ãããã³ã¼ããèªãã°ã©ããªã¯ã¨ãªãçºè¡ãããã¨ãã¦ãã®ãããã«è¦ããç¹ãããã§ãã
ããããªãããã¹ã¿ã¼ãã¢ããã§éçºã¹ãã¼ããè¦æ±ããã夿´ãå¤ãç°å¢ã®ãªãã§ã¯ãè¾ãç¹ãè¦ãã¦ãã¾ããã
Insert / Updateã®è¨è¿°ãè¾ã
ä¸ã«ãæ¸ãã¾ãããããã¼ãã«ãå¢ãã度ã«ä»¥ä¸ã®ãããªã¯ã¨ãªã¨ããããããã³ã°ããã³ã¼ããæ¯å1ããæ¸ãå¿ è¦ãããã®ã¯çµæ§ããã©ããã®ãããã¾ããã«ã©ã ãå¢ãããå¤ãã£ãå ´åã«ã¯ãä»ã®ã¯ã¨ãªã§ä½¿ã£ã¦ããinsert / updateæãå¿ããã«è¿½éãã¦ãããªããã°ããã¾ããã
query := ` insert into clinics( , name , create_timestamp , update_timestamp ) values ( , :name , :create_timestamp , :update_timestamp ) returning id `
ããããéçºåææ®µéã / 70%ã®æ©è½ã§ã¯ã·ã³ãã«ãªCRUDã§ååãªã±ã¼ã¹ãå¤ã
æè»ã«Selectæããããã¨ããã®ã¯ã¨ã¦ãè¯ãä½é¨ãªã®ã§ãããå ¨ã¦ã®ãã¼ãã«ã§Selectæãã¬ãããªæ¸ãå¿ è¦ããããã¨ããã¨ãããªãã¨ã¯ãªãã使çã«ã¯70%以ä¸ã®æ©è½ã§ã¯ã·ã³ãã«ãªCRUDãã·ã¥ãã¨å®ç¾ã§ããã°ããã§ååã¨ããæè¦ãããã¾ãããããä¸ã¤ãã¼ãã«ã追å ãã度ã«CRUDãªSQLã¨structã«ãããã³ã°ããã³ã¼ãã®ãã¤ã©ã¼ãã¬ã¼ãã大éã«æ¸ãå¿ è¦ãããã®ã¯ãç¹ã«æ°è¦æ©è½éçºã§2ã3ã®ãã¼ãã«ã追å ããå¿ è¦ãããå ´åã«ã¯çµæ§ã¹ãã¬ã¹ã§ããã
TypeSafeã§ã¯ãªã
æåã®åé¡ç¹ã«ãç¹ããã¾ãããCreateæãUpdateæãé·ã ã¨æ¸ãå¿ è¦ãããå²ã«ã«ã©ã åãTypoãããDBã¨å¯¾å¿ã®ç°ãªãåãStructå´ã«å®ç¾©ãã¦ãã¾ã£ãããã¦ãã³ã³ãã¤ã«æã«ã¯æ°ãä»ãã¾ãã(ããã Type Safeã§ãªãã¨è¨ã£ã¦ããã®ããããã¾ãããã»ã»ã»)ãã«ã©ã 追å ã夿´ãåã£ãå ´åã«ç¹ã«Insert / Updateæã§ééããªãããã«è¿½éãã¦ããã®ã¯ããªã大å¤ã«æãã¾ããã
SQLBoiler
ä¸è¨ã«ããã課é¡ãå ¨ã¦è§£æ±ºãã¤ã¤ãæ®éã«Selectæãæ¸ããã¨æãã°æ¸ãããããªãã¼ã«ã¯ãªãã ãããã¨èª¿ã¹ã¦ããã¨ããã§ã以ä¸ã®ããã°ã§ç´¹ä»ããã¦ããSQLBoilerã¨ãããã¼ã«ã«ãã©ãçãã¾ããã
Go の ORM / query builder 消耗日記 - blog.izum.in
SQLBoilerã¨ã¯
SQLBoiler is a tool to generate a Go ORM tailored to your database schema.
ã¨ããããã«ãDBã®ã¹ãã¼ãããã¨ã«ãã¼ãã«ã«å¯¾å¿ããstructã¨CRUDãªæä½ãæä¾ããfunctionãèªåçæãã¦ããã¾ãã
REATE TABLE pilots ( id integer NOT NULL, name text NOT NULL ); ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id); CREATE TABLE jets ( id integer NOT NULL, pilot_id integer NOT NULL, age integer NOT NULL, name text NOT NULL, color text NOT NULL ); ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id); ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id);
çæãããã³ã¼ãã¯ãããªæãã§ããããã¨ã¯å¥ã«Findçã®ã¡ã½ãããããããçæããã¾ãã
type Pilot struct { ID int `boil:"id" json:"id" toml:"id" yaml:"id"` Name string `boil:"name" json:"name" toml:"name" yaml:"name"` R *pilotR `boil:"-" json:"-" toml:"-" yaml:"-"` L pilotR `boil:"-" json:"-" toml:"-" yaml:"-"` } type pilotR struct { Licenses LicenseSlice Languages LanguageSlice Jets JetSlice } type Jet struct { ID int `boil:"id" json:"id" toml:"id" yaml:"id"` PilotID int `boil:"pilot_id" json:"pilot_id" toml:"pilot_id" yaml:"pilot_id"` Age int `boil:"age" json:"age" toml:"age" yaml:"age"` Name string `boil:"name" json:"name" toml:"name" yaml:"name"` Color string `boil:"color" json:"color" toml:"color" yaml:"color"` R *jetR `boil:"-" json:"-" toml:"-" yaml:"-"` L jetR `boil:"-" json:"-" toml:"-" yaml:"-"` } type jetR struct { Pilot *Pilot }
DBã¨ã®å¯¾å¿ã ãã§ã¯ãªãJSONã®ã¿ã°ãè¨è¿°ãã¦ãããã®ã§ãèªåã¯å¾è¿°ããéã使ã£ã¦ãã¾ãããDBã§åå¾ããçµæããã®ã¾ã¾ã¬ã¹ãã³ã¹ã¨ãã¦è¿ãã°ãããããªã·ã³ãã«ãªã¢ããªã§ã¯ä¾¿å©ããããã¾ãããå¤é¨ãã¼å¶ç´ããã¨ã«relationãèªåã§è²¼ã£ã¦ãããnullableãªãã®ã«ã¯ null
ããã±ã¼ã¸ãé©ç¨ãã¦ããã¾ãã
åºæ¬çãªãã¼ã¿ã®æä½
詳細ã¯READMEã«è²ãã¾ãããSQLBoilerã§ã³ã¼ããçæããã¨ä»¥ä¸ã®ãããªæä½ã¯å ¨ã¦èªåçæãããstructã¨é¢æ°ã ãã§å®è£ ãããã¨ãå¯è½ã«ãªãã¾ãã
// IDã§å¼ã clinicl, err := models.FindClinic(db, clinicID) // å ¨ã¦åå¾ãã clinics, err: = models.Clinics().All( db) // where clinics, err := models.Clinics(qm.Where("pref_id = ?", prefID), qm.And("name = ?", name)).One(db) // insert err :=clinicl.Insert(tx, boil.Infer()) // update _, err := clinic.UPdate(tx, boil.Infer()) // Relationships prefecture, err := clinic.Prefecture()
ã¾ãå¿ è¦ãªã¨ãã«ã¯sqlxã®ããã«SQLãæ¸ãã¦ãªãã¸ã§ã¯ãã«ãããã³ã°ããã¨ããææ³ãåããã¨ãå¯è½ã§ã
// Custom struct for selecting a subset of data type JetInfo struct { AgeSum int `boil:"age_sum"` Count int `boil:"juicy_count"` } var info JetInfo // Use query building err := models.NewQuery(Select("sum(age) as age_sum", "count(*) as juicy_count", From("jets"))).Bind(ctx, db, &info) // Use a raw query err := queries.Raw(`select sum(age) as "age_sum", count(*) as "juicy_count" from jets`).Bind(ctx, db, &info)
ãã®ãã¢ããªåãAPIã§ã®ä½¿ãæ¹
ActiveRecord-like productivity
ã¨èªã£ã¦ããããï¼ãã¯ãããã¾ããããSQLBoilerã¯èªåçæããstructã使ã£ã¦DBã®å
容ããã®ã¾ã¾JSONã«ãã¦è¿ãã¨ãããã¨ãå¯è½ãªä½ãã«ãªã£ã¦ãã¾ãããããã以ä¸ã®è¦³ç¹ããå
ã
åå¨ãã¦ããrepositoryã¬ã¤ã¤ãæ´»ç¨ãã¦SQLBoilerã®ä¾åç¯å²ãrepositoryããã±ã¼ã¸ã«ã¨ã©ãã¦ãã¾ãã
- ãã¨ãã¨åå¨ããrepositoryã¬ã¤ã¤ãæ´»ç¨ããã°ãsqlxããå®å ¨ã«ç§»è¡ãããã¨ãå¯è½ã§ãããã¨
- SQLBoilerã¸ã®ã¢ããªã±ã¼ã·ã§ã³ã®ä¾åãå¼·ããã¨ãSQLBoilerããè±å´ãããã¨ãé£ãããªããã¨
- Goè¨èªã®æ§è³ªä¸ãçæãããstructã®æåããªã¼ãã¼ã©ã¤ããããããªå®è£ ããããã¨ãé£ãããã¨
å®è£
ã¨ãã¦ã¯ä»¥ä¸ã®ãããªæãã«ãªã£ã¦ãããã¢ããªã±ã¼ã·ã§ã³ç¨ã®structã§ãã domain.Clinic
ããã®ã¾ã¾SQLBoilerã«æ¸¡ããmodels.Clinic
ã«ãããã³ã°ããã¾ãèªåçæããã models.Clinic
ãrepositoryã®å¤ã«ã¯åºããã« domain.Clinic
ãç´ãã¨ããæãã«ãªã£ã¦ãã¾ããsqlxã®ã¨ãã«SQLãæ¸ãã¦ããã®ããªãã¸ã§ã¯ãã®ãããã³ã°ã«å¤ãã£ã¦ãã¾ã£ã¦ããã¨ããã°ãããªã®ã§ãããGoã®ã³ã¼ãä¸ã§ãããã®ã§IDEã®ãµãã¼ãã§fill-structã¨ãã£ããã¼ã«ã使ã£ã¦ã¬ãã¨structãåãããã¨ãã§ãã¾ãããä½ããã¡ããã¨è£å®ãèãã®ã§SQLãçã§æ¸ããããã¨ããããããªãçç£æ§ããããã¾ããã
// CreateClinic ã¯ãªããã¯æ å ±ãç»é²ãã func (r *repository) CreateClinic(tx boil.Executor, clinic *domain.Clinic) (int, error) { model := models.Clinic{ Name: clinic.Name, CreateTimestamp: time.Now(), UpdateTimestamp: time.Now(), } err := model.Insert(tx, boil.Infer()) if err != nil { return 0, errors.WithStack(err) } return model.ID, nil } // FindClinicByID IDã§ã¯ãªããã¯ãæ¤ç´¢ãã func (r *repository) FindClinicByID(q boil.Executor, clinicID int) (*domain.Clinic, error) { model, err := models.FindClinic(q, int(clinicID)) if err != nil { return nil, errors.WithStack(err) } clinic := mapToClinic(*model) return clinic, nil }
structã®è©°ãæ¿ããããã®ã¯éå¹çã§ããã大éã®ãã¼ã¿ãå¦çããªããã°ãããªãã¨ãã«ã¯ããã©ã¼ãã³ã¹ä¸ã®åé¡ã¨ãªãå¯è½æ§ãããã¾ãããé常ã®ã¢ããªåãAPIã§ã¯åé¡ã«ãªãã±ã¼ã¹ã¯ããã»ã©å¤ããªãã®ã§ã¯ãªãã§ããããã
使ã£ã¦ã¿ãææ
å ã ã®çãã©ãããsqlxã§è¾ããæãã¦ããç®æã¯SQLBoilerã§ããªã楽ããããã¨ãã§ããããã«ãªãã¾ããã䏿¹ã§SQLBoilerãä¸è½ã§ã¯ãªããä¾ãã°ç¾å¨ã®ã¨ããLeft Outer Joinããµãã¼ããã¦ãã¾ããã
è¤éãªã¯ã¨ãªãçºè¡ããå¿ è¦ãããå ´åãä¸è¿°ããstructã®è©°ãæ¿ãã³ã¹ãã許容ã§ããªãå ´åã«ã¯ãsqlxã¯å¼ãç¶ããã鏿è¢ãªã®ããªã¨æãã¾ããå ´åã«ãã£ã¦ã¯ä½µç¨ããããªã®ããªã¼ã¨æãã¤ã¤ä¸ã¤ã®ã¢ããªã§ï¼ã¤ã®DBã©ã¤ãã©ãªãè¦æ±ããã®ã¯ãªãã»ã»ã»ã¿ãããªãã¨ãæã£ã¦ãã¾ãã
PRæ
ANNONE ã§ã¯ããããªæè¡ã使ã£ã¦éçºãã¦ã¾ãã
- iOSã¢ããª: Flutter(Dart), Swift
- ãµã¼ãã¼: Go, Ruby on Rails
- ããã³ã: TypeScript
- æ©æ¢°å¦ç¿: Python
- ã¤ã³ãã©: GCP, Firebase
å ¨æ¹é¢ã¨ã³ã¸ãã¢åéä¸ã®ãããªã®ã§ãèå³ã®ããæ¹ã¯ãã² @maeharin ããã«DMãï¼
ãªãªã³ããã¯ã®ç©´ãåããã®ã«1人足ããªãã誰ãå ¥ç¤¾ãã¦ãããªãããªã¼ pic.twitter.com/tF1vPbj1hu
— æ¤æ¾ æ£å¤ªé@ANNONE (@shotaro766) 2019å¹´11æ15æ¥